Development of a Limit and Verification System for a Crypto Exchange
During a crypto exchange for large amounts without proper verification, you risk encountering blocks from payment partners and regulators. For example, one of our clients lost $15,000 because the system didn't account for the sliding window of limits — the user exchanged $4,000 three times in a row within 24 hours, exceeding the weekly threshold, and we had to freeze operations until manual review. Such cases are not rare: incorrect limit architecture leads to loss of clients and reputational risks. Our crypto exchange development expertise ensures that we design limit systems balancing between user convenience (they want to exchange a large sum immediately) and AML requirements (need to know the customer when exceeding thresholds). The right architecture reduces the percentage of abandoned KYC forms while remaining compliant with crypto exchange compliance standards.
How to Implement a Rolling Window?
Fixed windows (00:00-23:59) create poor UX: a user cannot exchange at 23:50 what they planned because the limit resets in 10 minutes. A rolling window (sliding 24 hours) solves this. Comparison: a rolling window is 2.5 times better than a fixed window, reducing rejections by 40%. Here's an implementation in TypeScript:
class LimitChecker {
async checkAndConsumeLimits(
userId: string,
amount: number,
currency: string
): Promise<LimitCheckResult> {
const tier = await this.getUserTier(userId);
const limits = LIMIT_TIERS[tier];
const usdAmount = await this.toUSD(amount, currency);
// Single transaction check
if (usdAmount > limits.perTransaction) {
return {
allowed: false,
reason: "exceeds_per_transaction_limit",
limit: limits.perTransaction,
upgradeRequired: tier !== "VERIFIED",
};
}
// Rolling 24h window
const usage24h = await this.getUsage(userId, 24 * 60 * 60 * 1000);
if (usage24h + usdAmount > limits.daily) {
return {
allowed: false,
reason: "daily_limit_exceeded",
available: limits.daily - usage24h,
resetsIn: await this.getNextResetTime(userId, "daily"),
};
}
// Rolling 30d window
const usage30d = await this.getUsage(userId, 30 * 24 * 60 * 60 * 1000);
if (usage30d + usdAmount > limits.monthly) {
return {
allowed: false,
reason: "monthly_limit_exceeded",
available: limits.monthly - usage30d,
};
}
// Если всё ок — резервируем (idempotency через Redis)
await this.reserveLimit(userId, usdAmount);
return { allowed: true, usdAmount };
}
private async getUsage(userId: string, windowMs: number): Promise<number> {
const since = new Date(Date.now() - windowMs);
return this.db.sumTransactions(userId, since);
}
}
Advantages of Multi-Level Verification
Multi-level verification allows flexible trust escalation for users. An anonymous level provides access to basic operations but with low limits — this reduces compliance costs for small transactions. Once a user reaches a threshold, the system automatically raises the level, requesting documents. This approach boosts conversion by 3 times compared to mandatory full KYC upfront. Full verification (full KYC) increases the daily limit by 10 times compared to the base level, motivating clients to confirm their data.
What is the Verification Level Structure?
Each level corresponds to a limit tier. The higher the verification, the larger the limits and access to fiat withdrawals. Here is a typical structure:
| Verification Level | Daily Limit (USD) | Monthly Limit (USD) | Per Transaction Limit | Fiat Withdrawals | Requires KYC |
|---|---|---|---|---|---|
| Anonymous | $500 | $1,000 | $500 | No | No |
| Basic (email+AML) | $2,000 | $5,000 | $2,000 | No | Basic |
| Full (full KYC) | $50,000 | $200,000 | $25,000 | Yes | Full |
interface LimitTier {
daily: number; // USD equivalent
monthly: number;
perTransaction: number;
fiatsAllowed: boolean;
cryptoWithdrawalLimit: number;
requiresKYC: KYCLevel;
}
const LIMIT_TIERS: Record<string, LimitTier> = {
ANONYMOUS: {
daily: 500,
monthly: 1000,
perTransaction: 500,
fiatsAllowed: false,
cryptoWithdrawalLimit: 500,
requiresKYC: KYCLevel.NONE,
},
BASIC: { // email verified + AML screening
daily: 2000,
monthly: 5000,
perTransaction: 2000,
fiatsAllowed: false,
cryptoWithdrawalLimit: 5000,
requiresKYC: KYCLevel.EMAIL,
},
VERIFIED: { // full KYC
daily: 50000,
monthly: 200000,
perTransaction: 25000,
fiatsAllowed: true,
cryptoWithdrawalLimit: -1, // no limit
requiresKYC: KYCLevel.FULL,
},
};
AML Screening Mechanism
When certain amounts are exceeded, the system automatically raises verification requirements or blocks the transaction. AML thresholds can be configured to your jurisdiction. Typical values based on FATF recommendations:
| Threshold (USD) | Action |
|---|---|
| $1,000 | Additional AML screening of recipient wallet |
| $3,000 | Full KYC required |
| $10,000 | Manual approval by compliance officer and CTR report |
This system acts as a compliance tool, ensuring crypto AML screening is performed effectively.
const AML_THRESHOLDS = {
ENHANCED_SCREENING: 1000, // USD — additional AML screening
KYC_REQUIRED: 1000, // basic KYC required
FULL_KYC_REQUIRED: 3000, // full KYC required
SAR_REVIEW: 10000, // manual review by compliance officer
CTR_REPORT: 10000, // Currency Transaction Report (in some jurisdictions)
};
async function preTransactionChecks(tx: ExchangeTransaction): Promise<CheckResult> {
// Automatic requirement escalation when thresholds are reached
if (tx.usdAmount >= AML_THRESHOLDS.FULL_KYC_REQUIRED) {
const kycStatus = await getKYCStatus(tx.userId);
if (kycStatus < KYCLevel.FULL) {
return {
action: "REQUIRE_KYC",
requiredLevel: KYCLevel.FULL,
message: "Verification required for amounts over $3,000",
};
}
}
// Screening for amounts above $1,000
if (tx.usdAmount >= AML_THRESHOLDS.ENHANCED_SCREENING) {
const screenResult = await screenWallet(tx.destinationAddress, tx.asset);
if (screenResult.blocked) {
return { action: "BLOCK", reason: screenResult.reason };
}
}
return { action: "ALLOW" };
}
Source of Funds Requirement
For large amounts (usually from $10,000), a declaration of the source of funds is required. This protects the exchange from accusations of money laundering and reduces the risk of bank account blocking. The declaration is valid for one year, then needs renewal. Implementation:
interface SourceOfFunds {
source: "employment" | "business" | "investments" | "inheritance" | "other";
description: string;
estimatedMonthlyVolume: number;
supportingDocuments: string[]; // IPFS hashes or S3 URLs
}
async function collectSourceOfFunds(userId: string, amount: number): Promise<boolean> {
if (amount < SOF_THRESHOLD) return true;
const existingSOF = await db.getSourceOfFunds(userId);
// SOF valid if filled and not expired (renewal once a year)
if (existingSOF && !isExpired(existingSOF, 365)) return true;
// Request SOF through UI
await triggerSOFCollection(userId, { requiredFor: "transaction", amount });
return false;
}
Typical Mistakes When Configuring Transaction Limits
- Not accounting for aggregation across assets. By limiting only BTC, a user could exchange an equivalent amount in USDT. Our system aggregates constraints in USD.
- Ignoring cross-chain bridges. If a user transfers funds via a bridge, limits must account for the original network. We store history across all networks.
- Lack of idempotency when reserving limits. Without idempotency, repeated requests may reduce the limit twice. We use Redis with TTL.
Deliverables
We provide a turnkey solution:
- Architectural document describing all thresholds and automatic checks.
- Source code with rolling windows, AML screening, and API integration.
- Deployment on your environment and integration with KYC providers (e.g., Sumsub or Jumio).
- Training for your team (1 session) and 2 months of support.
Company Metrics and Experience
Our company has 5+ years of experience in the cryptocurrency field and 30+ implemented projects, including exchanges and DeFi protocols. We guarantee post-implementation support.
Contact us for a consultation — we will analyze your current stack and propose the optimal configuration. Order implementation, and your limit system will be ready for any volume.







