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.

Kevan Shah · · 13 min read

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:

  1. Accuracy: Do markets predict correctly?
  2. Engagement: Are users actively participating?
  3. 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:

  1. Accuracy: Brier scores, calibration, expert accuracy
  2. Engagement: DAU, retention, trading activity
  3. Efficiency: Liquidity depth, slippage, resolution times
  4. Financials: Revenue per user, protocol fees, sustainability
  5. 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.