Risk Management
Predictu employs a 5-wall defense system that evaluates every incoming trade before execution. Each wall acts as an independent gate - a trade must pass all five to proceed. If any wall rejects the trade, the entire request is denied and a risk event is logged with the appropriate severity.
Defense System Overview
The five walls are evaluated in order. Execution stops at the first rejection, which minimizes unnecessary computation. The walls progress from cheapest to most expensive in terms of query cost.
| Wall | Name | Scope | Default Limit |
|---|---|---|---|
1 | Per-trade limit | Single trade, per user tier | Varies by tier |
2 | Per-market exposure cap | Total platform exposure on one market | $10,000 |
3 | Per-category exposure cap | Total exposure across a category | $25,000 |
4 | Global exposure cap | Total platform-wide exposure | $100,000 |
5 | Circuit breakers | Per-user and system-wide halts | $5,000 daily loss / user |
Wall 1: Per-Trade Limits
Every user belongs to a tier. The tier determines the maximum dollar amount that can be risked on a single trade. Tiers are assigned automatically based on user behavior scoring and can be overridden manually by a God Mode admin.
Tier Limit Table
| Tier | Max Per-Trade | Description |
|---|---|---|
new | $10 | Default tier for newly registered users. Low limit while behavior is assessed. |
regular | $100 | Users with normal trading patterns. Upgraded automatically after sufficient history. |
vip | $1,000 | High-value users with clean history. Can be assigned manually by operators. |
restricted | $5 | Users flagged as sharp bettors or exhibiting suspicious patterns. |
How Tier is Evaluated
function getPerTradeLimit(user: User): number {
const tierLimits: Record<UserTier, number> = {
new: 10,
regular: 100,
vip: 1000,
restricted: 5,
};
return tierLimits[user.tier] ?? tierLimits.new;
}
// Wall 1 check
if (tradeAmount > getPerTradeLimit(user)) {
logRiskEvent("wall_1_rejected", {
severity: "warning",
user_id: user.id,
attempted: tradeAmount,
limit: getPerTradeLimit(user),
tier: user.tier,
});
throw new RiskError("Trade exceeds per-trade limit for your tier");
}Wall 2: Per-Market Exposure Cap
This wall prevents the platform from accumulating too much risk on any single market. The exposure is calculated as the sum of all open position costs across all users for a given market.
Calculation
const marketExposure = await getMarketExposure(marketId);
const MAX_MARKET_EXPOSURE = 10_000; // $10k default
if (marketExposure + tradeAmount > MAX_MARKET_EXPOSURE) {
logRiskEvent("wall_2_rejected", {
severity: "warning",
market_id: marketId,
current_exposure: marketExposure,
attempted: tradeAmount,
cap: MAX_MARKET_EXPOSURE,
});
throw new RiskError("Market exposure cap reached");
}Wall 3: Per-Category Exposure Cap
Markets are grouped into categories (e.g., politics, sports,crypto, entertainment). Wall 3 caps total exposure across all markets within a category at $25,000 by default.
Rationale
Correlated events within a category can create systemic risk. For example, if multiple political markets resolve the same way due to a single election result, the platform could face outsized losses. Category caps limit this correlation risk.
const categoryExposure = await getCategoryExposure(market.category);
const MAX_CATEGORY_EXPOSURE = 25_000; // $25k default
if (categoryExposure + tradeAmount > MAX_CATEGORY_EXPOSURE) {
logRiskEvent("wall_3_rejected", {
severity: "warning",
category: market.category,
current_exposure: categoryExposure,
attempted: tradeAmount,
cap: MAX_CATEGORY_EXPOSURE,
});
throw new RiskError("Category exposure cap reached");
}Wall 4: Global Exposure Cap
The final exposure wall caps total platform-wide risk at $100,000. This is the sum of all open positions across all markets, categories, and users. It acts as the ultimate backstop to prevent runaway exposure.
const globalExposure = await getGlobalExposure();
const MAX_GLOBAL_EXPOSURE = 100_000; // $100k default
if (globalExposure + tradeAmount > MAX_GLOBAL_EXPOSURE) {
logRiskEvent("wall_4_rejected", {
severity: "critical",
current_exposure: globalExposure,
attempted: tradeAmount,
cap: MAX_GLOBAL_EXPOSURE,
});
throw new RiskError("Global exposure cap reached");
}critical severity because it means the entire platform is at its risk limit. This should trigger an immediate admin notification.Wall 5: Circuit Breakers
Circuit breakers are dynamic halts that activate based on loss patterns. Unlike the exposure caps (Walls 2–4), circuit breakers monitor realized losses over rolling time windows.
Circuit Breaker Types
| Breaker | Trigger | Effect | Auto-Reset |
|---|---|---|---|
daily_loss_halt | User loses > $5,000 in 24h | Block all new buys for that user | After 24h window rolls |
rapid_loss_halt | User loses > $2,000 in 1h | Block all new buys for that user | After 1h window rolls |
system_halt | Platform loses > $50,000 in 24h | Block all new buys platform-wide | Manual reset by God Mode admin |
Configuration
interface CircuitBreakerConfig {
daily_loss_halt: {
threshold: number; // Default: 5000
window_hours: number; // Default: 24
scope: "user";
};
rapid_loss_halt: {
threshold: number; // Default: 2000
window_hours: number; // Default: 1
scope: "user";
};
system_halt: {
threshold: number; // Default: 50000
window_hours: number; // Default: 24
scope: "platform";
auto_reset: false; // Requires manual intervention
};
}Evaluation Flow
Sharp Bettor Detection
The user scoring system assigns a sharpness_score (0–100) based on historical trading patterns. Users with high scores consistently beat the spread, which indicates informed or professional betting. The risk engine uses this score to automatically widen spreads for sharp users.
Spread Adjustments
| Condition | Spread Adjustment | Reason |
|---|---|---|
Tier = restricted | +3% | Flagged as sharp bettor. Maximum spread penalty. |
| Sharpness score > 80 | +2% | Consistently profitable. Likely informed bettor. |
| Sharpness score > 60 | +1% | Above-average performance. Minor spread widening. |
| Sharpness score ≤ 60 | +0% (none) | Normal user. Standard spread applied. |
restricteduser with a score of 85 gets +3% (the tier penalty), not +3% + 2%. The highest applicable adjustment wins.Spread Calculation Example
function getSpreadAdjustment(user: User): number {
if (user.tier === "restricted") return 0.03;
if (user.sharpness_score > 80) return 0.02;
if (user.sharpness_score > 60) return 0.01;
return 0;
}
// Applied during price calculation
const baseSpread = spreadEngine.getSpread(market);
const adjustment = getSpreadAdjustment(user);
const effectiveSpread = baseSpread + adjustment;
// Example: base spread 4%, restricted user
// effectiveSpread = 0.04 + 0.03 = 0.07 (7%)
// Buy price for 50% market: 0.50 + (0.07 / 2) = 0.535
// Sell price for 50% market: 0.50 - (0.07 / 2) = 0.465Sells Always Bypass Risk
A core design decision: sell orders (position exits) never trigger risk walls.When a user sells, they are reducing their position and, by extension, the platform’s exposure. Blocking sells would increase risk, not decrease it.
function shouldApplyRiskChecks(side: "buy" | "sell"): boolean {
// Sells always pass - they reduce exposure
if (side === "sell") return false;
return true;
}daily_loss_halt can still close their open positions.Risk Event Logging
Every risk decision - whether a pass or rejection - is logged to therisk_events table. This provides a complete audit trail for compliance and debugging.
Severity Levels
| Severity | Meaning | Admin Action |
|---|---|---|
info | Trade passed all risk checks. Normal operation. | None. Available in logs for audit. |
warning | Trade rejected by Wall 1, 2, or 3. User or market at limit. | Review in operator dashboard. May indicate growing exposure. |
critical | Trade rejected by Wall 4 or 5. Platform-level risk event. | Immediate notification. God Mode admin should review. |
Risk Event Schema
interface RiskEvent {
id: string;
timestamp: string; // ISO 8601
severity: "info" | "warning" | "critical";
wall: 1 | 2 | 3 | 4 | 5 | null; // null for info (passed)
user_id: string;
operator_id: string | null;
market_id: string | null;
trade_amount: number;
details: {
current_exposure?: number;
cap?: number;
tier?: string;
sharpness_score?: number;
circuit_breaker?: string;
[key: string]: unknown;
};
}User Tier Changes
Tier changes are sensitive operations because they directly affect a user’s trading limits and spread adjustments. Every tier change is recorded with a full audit trail.
Tier Change Flow
user_tier_changes with the old tier, new tier, admin ID, reason, and timestamp.tier field is updated. The change takes effect immediately on all subsequent trades.Audit Record Schema
interface UserTierChange {
id: string;
user_id: string;
old_tier: UserTier;
new_tier: UserTier;
changed_by: string; // Admin user ID
reason: string; // Mandatory explanation
changed_at: string; // ISO 8601
source: "godmode" | "operator" | "automatic";
}restricted tier. The reason is recorded as"Automatic restriction: sharpness score exceeded threshold" and the source is set to automatic.Configuration Reference
All risk parameters can be configured per operator via the God Mode dashboard. Defaults are shown below.
interface RiskConfig {
// Wall 1: Per-trade limits
tier_limits: {
new: 10;
regular: 100;
vip: 1000;
restricted: 5;
};
// Wall 2: Per-market cap
max_market_exposure: 10_000;
// Wall 3: Per-category cap
max_category_exposure: 25_000;
// Wall 4: Global cap
max_global_exposure: 100_000;
// Wall 5: Circuit breakers
circuit_breakers: {
daily_loss_halt: 5_000;
rapid_loss_halt: 2_000;
system_halt: 50_000;
};
// Sharp bettor adjustments
spread_adjustments: {
restricted: 0.03;
sharp_high: 0.02; // score > 80
sharp_medium: 0.01; // score > 60
};
}Risk configuration changes are logged to the audit trail and require God Mode admin privileges. Operator admins can view their configuration but cannot modify it directly.
