Prediction Market Metrics and Analytics: Measuring Success
Track the right metrics for prediction market success. Learn about accuracy scoring, user engagement, liquidity health, and platform analytics.
Building successful prediction markets requires more than good technology—it demands rigorous measurement and optimization. The right metrics reveal user behavior, market efficiency, and platform health, enabling data-driven decisions that separate thriving platforms from failed experiments.
This guide identifies essential metrics for prediction market builders and explains how to track, analyze, and act on them.
Core Metrics Framework
The Three Pillars of Prediction Market Success:
- Accuracy: Do markets predict correctly?
- Engagement: Are users actively participating?
- Efficiency: Is the platform operating optimally?
Market Accuracy Metrics
Brier Score
The gold standard for prediction accuracy:
contract BrierScore {
function calculateBrierScore(
uint256 predictedProbability,
bool actualOutcome
) public pure returns (uint256) {
// Brier Score = (p - o)²
// Where p = predicted probability, o = actual outcome (0 or 1)
uint256 prediction = predictedProbability; // 0 to 1e18
uint256 outcome = actualOutcome ? 1e18 : 0;
uint256 diff = prediction > outcome ? prediction - outcome : outcome - prediction;
return (diff * diff) / 1e18; // Returns 0-1e18
}
function calculateMarketBrierScore(
uint256 marketId
) external view returns (uint256) {
Market memory market = markets[marketId];
require(market.resolved, "Market not resolved");
uint256 finalPrice = market.finalPrice;
bool actualOutcome = market.outcome == 1;
return calculateBrierScore(finalPrice, actualOutcome);
}
}
Interpretation:
- 0 = Perfect prediction
- 0.25 = Random guessing on binary market
- 1 = Worst possible prediction
Calibration Metrics
Do 70% predictions happen 70% of the time?
contract CalibrationMetrics {
struct CalibrationBucket {
uint256 lowerBound; // e.g., 0.6e18 for 60%
uint256 upperBound; // e.g., 0.7e18 for 70%
uint256 totalPredictions;
uint256 actualOccurrences;
}
CalibrationBucket[10] public buckets;
function recordPrediction(uint256 probability) external {
for (uint i = 0; i < buckets.length; i++) {
if (probability >= buckets[i].lowerBound &&
probability < buckets[i].upperBound) {
buckets[i].totalPredictions++;
break;
}
}
}
function recordOutcome(uint256 probability, bool occurred) external {
for (uint i = 0; i < buckets.length; i++) {
if (probability >= buckets[i].lowerBound &&
probability < buckets[i].upperBound) {
buckets[i].actualOccurrences += occurred ? 1 : 0;
break;
}
}
}
function getCalibrationScore() public view returns (int256) {
int256 totalDeviation = 0;
for (uint i = 0; i < buckets.length; i++) {
if (buckets[i].totalPredictions > 0) {
uint256 expectedRate = (buckets[i].lowerBound + buckets[i].upperBound) / 2;
uint256 actualRate = (buckets[i].actualOccurrences * 1e18) / buckets[i].totalPredictions;
int256 deviation = int256(expectedRate) - int256(actualRate);
totalDeviation += deviation > 0 ? deviation : -deviation;
}
}
return totalDeviation / int256(buckets.length);
}
}
Market Accuracy Ranking
contract AccuracyRanking {
struct UserAccuracy {
address user;
uint256 totalPredictions;
uint256 correctPredictions;
uint256 averageBrierScore;
uint256 reputationScore;
}
mapping(address => UserAccuracy) public userStats;
address[] public topPredictors;
function updateAccuracy(
address user,
uint256 marketId,
bool wasCorrect
) external {
UserAccuracy storage stats = userStats[user];
stats.totalPredictions++;
if (wasCorrect) {
stats.correctPredictions++;
}
// Calculate new average Brier score
uint256 marketBrier = calculateBrierScoreForUser(user, marketId);
stats.averageBrierScore = ((stats.averageBrierScore * (stats.totalPredictions - 1)) + marketBrier) / stats.totalPredictions;
// Update reputation (0-10000)
stats.reputationScore = calculateReputation(stats);
// Update top predictors list
_updateTopPredictors(user);
}
function calculateReputation(UserAccuracy memory stats) internal pure returns (uint256) {
if (stats.totalPredictions < 10) return 0;
// Base on accuracy and volume
uint256 accuracy = (stats.correctPredictions * 10000) / stats.totalPredictions;
uint256 volumeBonus = stats.totalPredictions > 100 ? 1000 : stats.totalPredictions * 10;
uint256 consistencyBonus = (10000 - stats.averageBrierScore) / 10;
return accuracy + volumeBonus + consistencyBonus;
}
}
User Engagement Metrics
Daily Active Users (DAU)
contract UserEngagement {
mapping(uint256 => uint256) public dailyActiveUsers;
mapping(address => uint256) public lastActiveDay;
function recordActivity(address user) internal {
uint256 today = block.timestamp / 1 days;
if (lastActiveDay[user] < today) {
dailyActiveUsers[today]++;
lastActiveDay[user] = today;
}
}
function getDAU(uint256 daysAgo) external view returns (uint256) {
uint256 targetDay = (block.timestamp / 1 days) - daysAgo;
return dailyActiveUsers[targetDay];
}
function getMAU() external view returns (uint256) {
uint256 uniqueUsers = 0;
uint256 currentDay = block.timestamp / 1 days;
for (uint i = 0; i < 30; i++) {
uniqueUsers += dailyActiveUsers[currentDay - i];
}
return uniqueUsers;
}
}
User Retention Cohorts
interface RetentionCohort {
signupWeek: string;
week0: number; // Users who signed up
week1: number; // Users active 1 week later
week2: number;
week4: number;
week8: number;
week12: number;
}
class RetentionAnalytics {
async calculateRetentionCohorts(): Promise<RetentionCohort[]> {
const cohorts: RetentionCohort[] = [];
// Get users grouped by signup week
const usersByWeek = await this.getUsersBySignupWeek();
for (const [week, users] of Object.entries(usersByWeek)) {
const cohort: RetentionCohort = {
signupWeek: week,
week0: users.length,
week1: await this.getActiveUsers(users, 1),
week2: await this.getActiveUsers(users, 2),
week4: await this.getActiveUsers(users, 4),
week8: await this.getActiveUsers(users, 8),
week12: await this.getActiveUsers(users, 12)
};
cohorts.push(cohort);
}
return cohorts;
}
async getActiveUsers(userIds: string[], weeksLater: number): Promise<number> {
const activeUsers = await db.query(`
SELECT COUNT(DISTINCT user_id)
FROM user_activity
WHERE user_id = ANY($1)
AND activity_date BETWEEN $2 AND $3
`, [
userIds,
addWeeks(weeksLater),
addWeeks(weeksLater + 1)
]);
return activeUsers;
}
}
Trading Activity Metrics
contract TradingMetrics {
struct DailyMetrics {
uint256 tradeCount;
uint256 volume;
uint256 uniqueTraders;
uint256 averageTradeSize;
uint256 newTraders;
}
mapping(uint256 => DailyMetrics) public dailyStats;
mapping(address => bool) public hasTraded;
function recordTrade(address trader, uint256 amount) internal {
uint256 today = block.timestamp / 1 days;
DailyMetrics storage stats = dailyStats[today];
stats.tradeCount++;
stats.volume += amount;
if (!hasTraded[trader]) {
stats.newTraders++;
hasTraded[trader] = true;
}
// Unique traders approximation
if (stats.tradeCount % 100 == 0) {
stats.uniqueTraders = countUniqueTraders(today);
}
}
function getAverageTradeSize(uint256 daysAgo) external view returns (uint256) {
uint256 day = (block.timestamp / 1 days) - daysAgo;
DailyMetrics memory stats = dailyStats[day];
return stats.tradeCount > 0 ? stats.volume / stats.tradeCount : 0;
}
}
Liquidity Health Metrics
Slippage Analysis
contract SlippageMetrics {
struct SlippageReading {
uint256 timestamp;
uint256 tradeSize;
uint256 slippageBps;
}
mapping(uint256 => SlippageReading[]) public marketSlippage;
function recordSlippage(
uint256 marketId,
uint256 tradeSize,
uint256 expectedPrice,
uint256 executedPrice
) external {
uint256 slippage = expectedPrice > executedPrice
? ((expectedPrice - executedPrice) * 10000) / expectedPrice
: ((executedPrice - expectedPrice) * 10000) / executedPrice;
marketSlippage[marketId].push(SlippageReading({
timestamp: block.timestamp,
tradeSize: tradeSize,
slippageBps: slippage
}));
// Alert if slippage too high
if (slippage > 500) { // 5%
emit HighSlippageAlert(marketId, tradeSize, slippage);
}
}
function getAverageSlippage(uint256 marketId, uint256 lookbackHours)
external view returns (uint256) {
SlippageReading[] storage readings = marketSlippage[marketId];
uint256 cutoffTime = block.timestamp - (lookbackHours * 3600);
uint256 totalSlippage = 0;
uint256 count = 0;
for (uint i = readings.length; i > 0; i--) {
if (readings[i-1].timestamp < cutoffTime) break;
totalSlippage += readings[i-1].slippageBps;
count++;
}
return count > 0 ? totalSlippage / count : 0;
}
}
Liquidity Depth
contract LiquidityMetrics {
function getLiquidityDepth(uint256 marketId) external view returns (
uint256 yesDepth,
uint256 noDepth,
uint256 totalDepth
) {
Market memory market = markets[marketId];
// Calculate depth at different price levels
yesDepth = calculateDepth(market.yesBook);
noDepth = calculateDepth(market.noBook);
totalDepth = yesDepth + noDepth;
}
function calculateDepth(OrderBook storage book) internal view returns (uint256) {
uint256 depth = 0;
for (uint i = 0; i < book.orders.length; i++) {
depth += book.orders[i].amount;
}
return depth;
}
function getBidAskSpread(uint256 marketId) external view returns (uint256) {
Market memory market = markets[marketId];
uint256 bestBid = getBestBid(market.yesBook);
uint256 bestAsk = getBestAsk(market.yesBook);
return bestAsk - bestBid;
}
}
Financial Metrics
Protocol Revenue
contract RevenueMetrics {
struct DailyRevenue {
uint256 tradingFees;
uint256 withdrawalFees;
uint256 resolutionFees;
uint256 totalRevenue;
}
mapping(uint256 => DailyRevenue) public dailyRevenue;
uint256 public totalRevenue;
function recordTradingFee(uint256 amount) internal {
uint256 today = block.timestamp / 1 days;
dailyRevenue[today].tradingFees += amount;
dailyRevenue[today].totalRevenue += amount;
totalRevenue += amount;
}
function getRevenueByPeriod(uint256 startDay, uint256 endDay)
external view returns (uint256) {
uint256 revenue = 0;
for (uint day = startDay; day <= endDay; day++) {
revenue += dailyRevenue[day].totalRevenue;
}
return revenue;
}
function getRevenuePerUser(uint256 daysAgo) external view returns (uint256) {
uint256 revenue = dailyRevenue[(block.timestamp / 1 days) - daysAgo].totalRevenue;
uint256 users = getDAU(daysAgo);
return users > 0 ? revenue / users : 0;
}
}
User P&L Tracking
contract UserPnLMetrics {
struct UserPnL {
uint256 totalDeposits;
uint256 totalWithdrawals;
uint256 realizedPnL;
uint256 unrealizedPnL;
uint256 winCount;
uint256 lossCount;
uint256 largestWin;
uint256 largestLoss;
}
mapping(address => UserPnL) public userPnL;
function updateRealizedPnL(
address user,
int256 pnl
) external {
UserPnL storage stats = userPnL[user];
if (pnl > 0) {
stats.realizedPnL += uint256(pnl);
stats.winCount++;
if (uint256(pnl) > stats.largestWin) {
stats.largestWin = uint256(pnl);
}
} else {
stats.realizedPnL -= uint256(-pnl);
stats.lossCount++;
if (uint256(-pnl) > stats.largestLoss) {
stats.largestLoss = uint256(-pnl);
}
}
}
function getWinRate(address user) external view returns (uint256) {
UserPnL memory stats = userPnL[user];
uint256 totalTrades = stats.winCount + stats.lossCount;
return totalTrades > 0 ? (stats.winCount * 10000) / totalTrades : 0;
}
}
Market Quality Metrics
Price Efficiency
contract PriceEfficiency {
// Measure how quickly prices incorporate new information
struct PriceUpdate {
uint256 timestamp;
uint256 price;
uint256 volume;
}
mapping(uint256 => PriceUpdate[]) public priceHistory;
function calculatePriceImpact(
uint256 marketId,
uint256 tradeVolume
) external view returns (uint256) {
PriceUpdate[] storage history = priceHistory[marketId];
if (history.length < 2) return 0;
// Find trades of similar size
uint256 totalImpact = 0;
uint256 count = 0;
for (uint i = 1; i < history.length; i++) {
if (history[i].volume >= tradeVolume * 0.8 &&
history[i].volume <= tradeVolume * 1.2) {
uint256 priceChange = history[i].price > history[i-1].price
? history[i].price - history[i-1].price
: history[i-1].price - history[i].price;
totalImpact += priceChange;
count++;
}
}
return count > 0 ? totalImpact / count : 0;
}
}
Market Resolution Time
contract ResolutionMetrics {
struct ResolutionTiming {
uint256 scheduledTime;
uint256 actualResolutionTime;
uint256 delay;
}
mapping(uint256 => ResolutionTiming) public resolutions;
function recordResolution(
uint256 marketId,
uint256 scheduledTime
) external {
resolutions[marketId] = ResolutionTiming({
scheduledTime: scheduledTime,
actualResolutionTime: block.timestamp,
delay: block.timestamp > scheduledTime ? block.timestamp - scheduledTime : 0
});
}
function getAverageResolutionDelay(uint256 lookbackMarkets)
external view returns (uint256) {
uint256 totalDelay = 0;
uint256 count = 0;
for (uint i = marketCount; i > marketCount - lookbackMarkets && i > 0; i--) {
if (resolutions[i].actualResolutionTime > 0) {
totalDelay += resolutions[i].delay;
count++;
}
}
return count > 0 ? totalDelay / count : 0;
}
}
Dashboard Implementation
Real-Time Analytics Dashboard
interface PlatformDashboard {
// Overview Metrics
totalMarkets: number;
activeMarkets: number;
totalVolume24h: number;
totalLiquidity: number;
// User Metrics
dailyActiveUsers: number;
monthlyActiveUsers: number;
newUsers24h: number;
userRetentionRate: number;
// Market Metrics
averageBrierScore: number;
calibrationScore: number;
marketsResolvingThisWeek: number;
averageLiquidityDepth: number;
// Financial Metrics
revenue24h: number;
revenue7d: number;
averageRevenuePerUser: number;
protocolFeesCollected: number;
// Health Metrics
averageSlippage: number;
marketsNeedingLiquidity: string[];
oracleHealth: 'healthy' | 'degraded' | 'critical';
circuitBreakersActive: number;
}
class AnalyticsDashboard {
async getDashboardData(): Promise<PlatformDashboard> {
const [
marketStats,
userStats,
financialStats,
healthStats
] = await Promise.all([
this.getMarketStats(),
this.getUserStats(),
this.getFinancialStats(),
this.getHealthStats()
]);
return {
...marketStats,
...userStats,
...financialStats,
...healthStats
};
}
}
Alerting and Monitoring
Automated Alerts
class AlertSystem {
async checkAlerts(): Promise<void> {
// Check for anomalies
await this.checkVolumeAnomalies();
await this.checkLiquidityCrises();
await this.checkOracleDelays();
await this.checkUserComplaints();
// Check for opportunities
await this.checkUnderperformingMarkets();
await this.checkLiquidityOpportunities();
}
async checkLiquidityCrises(): Promise<void> {
const markets = await this.getActiveMarkets();
for (const market of markets) {
const liquidity = await this.getMarketLiquidity(market.id);
const slippage = await this.getAverageSlippage(market.id, 24);
if (liquidity < 10000 && slippage > 500) {
await this.sendAlert({
severity: 'high',
type: 'liquidity_crisis',
marketId: market.id,
message: `Market ${market.id} has critically low liquidity`
});
}
}
}
}
Best Practices
1. Data Collection
- Log everything at the contract level
- Use events for off-chain indexing
- Maintain data history
- Ensure data integrity
2. Privacy Considerations
contract PrivacyPreservingMetrics {
// Aggregate data to protect individual privacy
function getAggregatedPnL(uint256 cohortSize) external view returns (int256) {
// Return average P&L for groups, not individuals
}
function getVolumePercentile(uint256 percentile) external view returns (uint256) {
// Return volume at percentile without revealing individual data
}
}
3. Performance Optimization
- Use off-chain indexing for complex queries
- Cache frequently accessed metrics
- Batch data collection
- Archive old data
4. Actionable Insights
Metrics should drive decisions:
- Low liquidity → Increase LP rewards
- High slippage → Add more liquidity
- Poor accuracy → Improve oracle selection
- Low retention → Enhance UX
Conclusion
Comprehensive analytics separate successful prediction markets from failures. Track everything, but focus on:
- Accuracy: Brier scores, calibration, expert accuracy
- Engagement: DAU, retention, trading activity
- Efficiency: Liquidity depth, slippage, resolution times
- Financials: Revenue per user, protocol fees, sustainability
- Health: System performance, security, user satisfaction
The prediction markets that win will be those that measure obsessively, analyze rigorously, and optimize continuously based on data—not guesswork.
Remember: You can't improve what you don't measure. Invest heavily in analytics infrastructure from day one.