Resolution & Settlement
When a prediction market reaches its end date or an outcome becomes known, the market enters the resolution phase. Predictu supports two resolution paths: resolveMarket (normal outcome determination with payouts) and voidMarket (cancellation with full refunds). Both are initiated by a God Mode admin or automatically via an oracle trigger.
Resolve Market Flow
The resolveMarket function is the primary settlement path. It determines winners and losers, calculates payouts, credits winners, notifies losers, and records the complete settlement.
Step-by-Step Flow
open or closed status. Markets already resolved or voided are rejected.resolved_outcome.market_id matches and status = 'open'. This includes positions from all users across all operators.outcome matches the resolved_outcome.quantity (number of shares held).walletAdapter.creditForWin(userId, payout, positionId). The wallet adapter sends a BET_WIN S2S callback to the operator’s server.walletAdapter.notifyLoss(userId, costBasis, positionId). This sends a BET_LOSE notification to the operator’s server.settlement row is inserted with aggregate statistics about the resolution.status = 'resolved' with the payout amount recorded.status = 'resolved' with the resolution timestamp.Implementation
async function resolveMarket(
marketId: string,
winningOutcome: string,
resolvedBy: string
): Promise<SettlementRecord> {
return await db.transaction(async (tx) => {
// Step 1: Load and validate market
const market = await tx.getMarket(marketId);
if (market.status === "resolved" || market.status === "voided") {
throw new Error("Market already settled");
}
// Step 2: Set winning outcome
await tx.updateMarket(marketId, {
resolved_outcome: winningOutcome,
resolved_by: resolvedBy,
resolved_at: new Date().toISOString(),
});
// Step 3: Fetch all open positions
const positions = await tx.getPositions({
market_id: marketId,
status: "open",
});
// Step 4 & 5: Classify and calculate
const winners = positions.filter(
(p) => p.outcome === winningOutcome
);
const losers = positions.filter(
(p) => p.outcome !== winningOutcome
);
let totalPayout = 0;
// Step 6: Credit winners ($1/share)
for (const position of winners) {
const payout = position.quantity; // $1 per share
const adapter = getWalletAdapter(position.operator_id);
await adapter.creditForWin(
position.user_id,
payout,
position.id
);
totalPayout += payout;
}
// Step 7: Notify losers ($0 payout)
for (const position of losers) {
const adapter = getWalletAdapter(position.operator_id);
await adapter.notifyLoss(
position.user_id,
position.cost_basis,
position.id
);
}
// Step 8: Create settlement record
const totalCostBasis = positions.reduce(
(sum, p) => sum + p.cost_basis, 0
);
const settlement = await tx.createSettlement({
market_id: marketId,
resolved_outcome: winningOutcome,
total_positions: positions.length,
winners_count: winners.length,
losers_count: losers.length,
total_payout: totalPayout,
total_cost_basis: totalCostBasis,
casino_profit: totalCostBasis - totalPayout,
resolved_by: resolvedBy,
});
// Step 9: Mark positions resolved
for (const position of positions) {
const payout = position.outcome === winningOutcome
? position.quantity
: 0;
await tx.updatePosition(position.id, {
status: "resolved",
payout_amount: payout,
resolved_at: new Date().toISOString(),
});
}
// Step 10: Update market status
await tx.updateMarket(marketId, { status: "resolved" });
return settlement;
});
}Payout Calculation
Predictu uses a binary outcome model. Shares are priced between $0 and $1 based on implied probability. At resolution, the math is simple:
| Scenario | Payout Per Share | Example |
|---|---|---|
| Winner (outcome matches) | $1.00 | Bought 50 shares at $0.60 each ($30 cost). Receives $50. Profit: $20. |
| Loser (outcome does not match) | $0.00 | Bought 50 shares at $0.40 each ($20 cost). Receives $0. Loss: $20. |
Casino Profit Calculation
// Casino profit = total money collected - total money paid out
// total_cost_basis = sum of all position cost bases (money collected)
// total_payout = sum of all winner payouts (money paid out)
casino_profit = total_cost_basis - total_payout
// Example market:
// 100 "Yes" shares sold at avg $0.65 = $65 collected
// 80 "No" shares sold at avg $0.35 = $28 collected
// Total cost basis = $93
//
// If "Yes" wins: pay 100 × $1 = $100. Profit = $93 - $100 = -$7 (loss)
// If "No" wins: pay 80 × $1 = $80. Profit = $93 - $80 = $13 (profit)Void Market Flow
The voidMarket function cancels a market and refunds every user their original cost basis. This is used when a market is created in error, an event is cancelled, or the outcome becomes unknowable.
Step-by-Step Flow
walletAdapter.refund(userId, costBasis, positionId). Every user gets back exactly what they paid, regardless of which outcome they bet on.casino_profit = 0 and total_payout = total_cost_basis.status = 'voided' with payout_amount = cost_basis.status = 'voided'.Implementation
async function voidMarket(
marketId: string,
reason: string,
voidedBy: string
): Promise<SettlementRecord> {
return await db.transaction(async (tx) => {
const market = await tx.getMarket(marketId);
if (market.status === "resolved" || market.status === "voided") {
throw new Error("Market already settled");
}
const positions = await tx.getPositions({
market_id: marketId,
status: "open",
});
let totalRefunded = 0;
// Refund every position at cost basis
for (const position of positions) {
const adapter = getWalletAdapter(position.operator_id);
await adapter.refund(
position.user_id,
position.cost_basis,
position.id
);
totalRefunded += position.cost_basis;
}
const settlement = await tx.createSettlement({
market_id: marketId,
resolved_outcome: null,
void_reason: reason,
total_positions: positions.length,
winners_count: 0,
losers_count: 0,
total_payout: totalRefunded,
total_cost_basis: totalRefunded,
casino_profit: 0,
resolved_by: voidedBy,
});
for (const position of positions) {
await tx.updatePosition(position.id, {
status: "voided",
payout_amount: position.cost_basis,
resolved_at: new Date().toISOString(),
});
}
await tx.updateMarket(marketId, {
status: "voided",
void_reason: reason,
});
return settlement;
});
}Settlement Record Schema
Every resolution or void creates a settlement record that captures the complete financial summary. These records are used for operator invoicing, revenue reporting, and audit trails.
interface SettlementRecord {
id: string; // UUID
market_id: string; // Market that was settled
resolved_outcome: string | null; // Winning outcome (null for voids)
void_reason: string | null; // Reason for voiding (null for resolves)
total_positions: number; // Total positions settled
winners_count: number; // Positions that won (0 for voids)
losers_count: number; // Positions that lost (0 for voids)
total_payout: number; // Total amount paid out
total_cost_basis: number; // Total amount originally collected
casino_profit: number; // cost_basis - payout
resolved_by: string; // Admin user ID who triggered settlement
created_at: string; // ISO 8601 timestamp
}Example Settlement Records
| Field | Resolve Example | Void Example |
|---|---|---|
resolved_outcome | "Yes" | null |
void_reason | null | "Event cancelled" |
total_positions | 180 | 180 |
winners_count | 100 | 0 |
losers_count | 80 | 0 |
total_payout | $100.00 | $93.00 |
total_cost_basis | $93.00 | $93.00 |
casino_profit | -$7.00 | $0.00 |
Wallet Adapter & Settlement
The wallet adapter pattern abstracts settlement payouts so the resolution engine does not need to know the details of the operator’s wallet implementation.
Adapter Methods Used During Settlement
| Method | Called When | S2S Callback |
|---|---|---|
creditForWin | Position won | Sends BET_WIN callback to operator |
notifyLoss | Position lost | Sends BET_LOSE callback to operator |
refund | Market voided | Sends BET_REFUND callback to operator |
settlement_pending and flagged for manual review in the God Mode dashboard. The overall transaction is not rolled back to avoid blocking settlement for other users.Post-Settlement
After settlement completes, several downstream processes are triggered:
- User scoring update - The scoring engine recalculates sharpness scores based on the settlement results. Users who consistently pick winners may see their score increase.
- Operator revenue calculation - The settlement’s
casino_profitis attributed to the appropriate operator(s) for invoicing. - Dashboard refresh - Both God Mode and Operator dashboards update their revenue metrics in near real-time.
- Risk exposure update - The resolved market’s exposure is released from all risk walls, freeing capacity for new trades.
Settlement is a one-way operation. Once a market is resolved or voided, it cannot be un-resolved. If a resolution was made in error, the only recourse is to create manual adjustments via the God Mode dashboard.
