Prediction Market Fundamentals: Core Mechanics and Market Design

Master the core mechanics of prediction markets. Learn market design principles, pricing mechanisms, and how to build robust prediction market infrastructure.

Kevan Shah · · 15 min read

Prediction markets represent one of blockchain's most compelling use cases—harnessing collective intelligence to forecast future events with remarkable accuracy. From political elections to crypto prices, weather patterns to corporate earnings, prediction markets aggregate dispersed information into actionable probabilities.

For builders entering this space, understanding the fundamental mechanics is essential. This comprehensive guide covers everything from basic market structures to advanced pricing mechanisms.

What Are Prediction Markets?

Prediction markets are markets where participants trade contracts whose payoff depends on the outcome of future events. Unlike traditional betting, these markets create tradable instruments that reflect the market's collective belief about probability.

Key Characteristics:

  • Binary outcomes: Will Event X happen by Date Y? (Yes/No)
  • Continuous pricing: Prices reflect real-time probability estimates
  • Liquidity aggregation: Market depth improves accuracy
  • Information revelation: Trading reveals private information
  • Settlement: Contracts resolve when outcome is determined

Market Types and Structures

Binary Markets

The simplest form—yes/no outcomes:

contract BinaryPredictionMarket {
    struct Market {
        string question;
        uint256 resolutionTime;
        uint256 resolutionPrice; // 0 or 1 after resolution
        bool resolved;
        uint256 yesShares;
        uint256 noShares;
    }
    
    mapping(uint256 => Market) public markets;
    uint256 public marketCount;
}

Scalar Markets

Predict specific values within a range:

contract ScalarPredictionMarket {
    struct ScalarMarket {
        string question;
        uint256 resolutionTime;
        uint256 lowerBound; // Minimum possible value
        uint256 upperBound; // Maximum possible value
        uint256 resolvedValue; // Actual value at resolution
        bool resolved;
    }
    
    // Payout varies based on where outcome falls in range
    function calculatePayout(
        uint256 marketId,
        uint256 position,
        uint256 amount
    ) external view returns (uint256) {
        ScalarMarket memory market = markets[marketId];
        require(market.resolved, "Not resolved");
        
        // Linear payout between bounds
        if (market.resolvedValue <= position) {
            return amount;
        } else {
            // Partial payout based on distance from position
            uint256 distance = position > market.resolvedValue 
                ? position - market.resolvedValue
                : market.resolvedValue - position;
            return amount * (market.upperBound - market.lowerBound - distance) 
                / (market.upperBound - market.lowerBound);
        }
    }
}

Categorical Markets

Multiple mutually exclusive outcomes:

contract CategoricalPredictionMarket {
    struct CategoricalMarket {
        string question;
        string[] outcomes;
        uint256 resolutionTime;
        uint256 winningOutcome;
        bool resolved;
        mapping(uint256 => uint256) shares; // outcome => shares
    }
    
    function createMarket(
        string memory question,
        string[] memory outcomes,
        uint256 resolutionTime
    ) external returns (uint256 marketId) {
        require(outcomes.length >= 2, "Need at least 2 outcomes");
        require(outcomes.length <= 8, "Maximum 8 outcomes");
        
        marketId = marketCount++;
        CategoricalMarket storage market = markets[marketId];
        market.question = question;
        market.resolutionTime = resolutionTime;
        
        for (uint i = 0; i < outcomes.length; i++) {
            market.outcomes.push(outcomes[i]);
        }
        
        emit MarketCreated(marketId, question, outcomes, resolutionTime);
    }
}

Automated Market Makers (AMMs) for Prediction Markets

Traditional order books struggle with prediction markets due to liquidity fragmentation. AMMs solve this by algorithmically pricing trades.

Constant Product Market Maker (CPMM)

contract PredictionMarketCPMM {
    struct Market {
        uint256 yesShares;
        uint256 noShares;
        uint256 constantK;
    }
    
    function getPrice(uint256 marketId) public view returns (uint256) {
        Market memory market = markets[marketId];
        // Price of YES = NO shares / (YES shares + NO shares)
        return (market.noShares * 1e18) / (market.yesShares + market.noShares);
    }
    
    function buyYes(uint256 marketId, uint256 collateralAmount) external {
        Market storage market = markets[marketId];
        
        // Transfer collateral
        collateralToken.transferFrom(msg.sender, address(this), collateralAmount);
        
        // Calculate YES shares received using constant product formula
        uint256 newYesShares = market.yesShares + collateralAmount;
        uint256 newNoShares = market.constantK / newYesShares;
        uint256 noSharesToSell = market.noShares - newNoShares;
        
        // User gets YES shares by effectively selling NO shares
        uint256 yesSharesReceived = collateralAmount + noSharesToSell;
        
        market.yesShares = newYesShares;
        market.noShares = newNoShares;
        
        // Mint YES shares to user
        _mintYesShares(msg.sender, yesSharesReceived);
    }
}

Logarithmic Market Scoring Rule (LMSR)

Hanson's LMSR provides bounded loss for market makers:

contract LMSRMarketMaker {
    uint256 public b; // Liquidity parameter
    
    function costFunction(
        uint256[] memory quantities
    ) public view returns (uint256) {
        uint256 sum = 0;
        for (uint i = 0; i < quantities.length; i++) {
            sum += exp(quantities[i] / b);
        }
        return b * ln(sum);
    }
    
    function getPrice(
        uint256[] memory quantities,
        uint256 outcomeIndex
    ) public view returns (uint256) {
        uint256 numerator = exp(quantities[outcomeIndex] / b);
        uint256 denominator = 0;
        
        for (uint i = 0; i < quantities.length; i++) {
            denominator += exp(quantities[i] / b);
        }
        
        return (numerator * 1e18) / denominator;
    }
    
    function calculateTradeCost(
        uint256[] memory currentQuantities,
        uint256[] memory newQuantities
    ) external view returns (uint256) {
        return costFunction(newQuantities) - costFunction(currentQuantities);
    }
}

Advantages of LMSR:

  • Bounded loss for market creator
  • Natural price formation
  • Liquidity provided algorithmically

Disadvantages:

  • Requires setting liquidity parameter (b)
  • Higher complexity
  • Gas intensive

Order Book vs AMM Trade-offs

FeatureOrder BookAMM
LiquidityRequires makersAlgorithmic
Price discoveryEfficient with depthFormula-based
MEV exposureHighModerate
Gas costsLower for makersPredictable
ScalabilityFragmented by marketUnified pools
Best forLarge marketsLong-tail markets

Hybrid Approaches

Some protocols combine both:

contract HybridPredictionMarket {
    struct Market {
        // AMM component
        uint256 yesLiquidity;
        uint256 noLiquidity;
        
        // Order book component
        mapping(uint256 => Order[]) buyOrders; // price => orders
        mapping(uint256 => Order[]) sellOrders; // price => orders
        
        uint256 ammThreshold; // Switch to AMM below this volume
    }
    
    function executeTrade(
        uint256 marketId,
        bool isBuy,
        uint256 amount,
        uint256 price
    ) external {
        if (amount < markets[marketId].ammThreshold) {
            // Use order book
            _executeOrderBookTrade(marketId, isBuy, amount, price);
        } else {
            // Use AMM
            _executeAMMTrade(marketId, isBuy, amount);
        }
    }
}

Market Creation and Curation

Permissionless Market Creation

Anyone can create markets:

function createMarket(
    string memory question,
    string memory description,
    uint256 resolutionTime,
    address oracle,
    uint256 initialLiquidity
) external payable returns (uint256 marketId) {
    require(msg.value >= MINIMUM_LIQUIDITY, "Insufficient liquidity");
    require(resolutionTime > block.timestamp + 1 days, "Resolution too soon");
    require(bytes(question).length > 0, "Empty question");
    
    marketId = marketCount++;
    
    markets[marketId] = Market({
        creator: msg.sender,
        question: question,
        description: description,
        resolutionTime: resolutionTime,
        oracle: oracle,
        resolved: false,
        outcome: 0,
        totalLiquidity: msg.value,
        createdAt: block.timestamp
    });
    
    // Initialize AMM with provided liquidity
    _initializeLiquidity(marketId, msg.value);
    
    emit MarketCreated(marketId, msg.sender, question, resolutionTime);
}

Curated Market Creation

Require approval for market creation:

contract CuratedPredictionMarket {
    address public curator;
    mapping(uint256 => bool) public approvedMarkets;
    
    function proposeMarket(
        string memory question,
        uint256 resolutionTime,
        address oracle
    ) external returns (uint256 proposalId) {
        proposalId = proposalCount++;
        proposals[proposalId] = MarketProposal({
            proposer: msg.sender,
            question: question,
            resolutionTime: resolutionTime,
            oracle: oracle,
            approved: false
        });
        
        emit MarketProposed(proposalId, msg.sender, question);
    }
    
    function approveMarket(uint256 proposalId) external {
        require(msg.sender == curator, "Only curator");
        proposals[proposalId].approved = true;
        
        // Now anyone can create the market
        emit MarketApproved(proposalId);
    }
}

Bond-Based Curation

Users stake bonds to create markets, slashed if market is invalid:

contract BondCuratedMarket {
    uint256 public constant CREATION_BOND = 1000e18; // 1000 tokens
    
    function createMarketWithBond(
        string memory question,
        uint256 resolutionTime
    ) external returns (uint256 marketId) {
        // Transfer bond
        token.transferFrom(msg.sender, address(this), CREATION_BOND);
        
        marketId = _createMarket(question, resolutionTime);
        marketBonds[marketId] = CREATION_BOND;
        
        // Bond returned after successful resolution
        emit MarketCreatedWithBond(marketId, msg.sender, CREATION_BOND);
    }
    
    function returnBond(uint256 marketId) external {
        require(markets[marketId].resolved, "Not resolved");
        require(!markets[marketId].invalid, "Market invalid");
        
        uint256 bond = marketBonds[marketId];
        marketBonds[marketId] = 0;
        
        token.transfer(markets[marketId].creator, bond);
    }
    
    function slashBond(uint256 marketId) external {
        require(msg.sender == disputeResolver, "Not authorized");
        require(markets[marketId].invalid, "Market not invalid");
        
        uint256 bond = marketBonds[marketId];
        marketBonds[marketId] = 0;
        
        // Send to treasury or burn
        treasury.deposit(bond);
    }
}

Fee Structures

Sustainable fee models balance trader incentives with protocol revenue.

Trading Fees

contract FeeStructure {
    uint256 public tradingFeeBps = 20; // 0.2%
    address public feeRecipient;
    
    function calculateFee(uint256 amount) public view returns (uint256) {
        return (amount * tradingFeeBps) / 10000;
    }
    
    function executeTradeWithFee(uint256 amount) internal returns (uint256 amountAfterFee) {
        uint256 fee = calculateFee(amount);
        amountAfterFee = amount - fee;
        
        // Send fee to recipient
        collateralToken.transfer(feeRecipient, fee);
    }
}

Dynamic Fees

Adjust based on market conditions:

function calculateDynamicFee(
    uint256 volume24h,
    uint256 marketAge
) public view returns (uint256) {
    // Lower fees for high volume markets
    uint256 volumeDiscount = volume24h > HIGH_VOLUME_THRESHOLD ? 50 : 0; // 50% discount
    
    // Lower fees for mature markets
    uint256 ageDiscount = marketAge > 30 days ? 25 : 0; // 25% discount
    
    uint256 baseFee = 30; // 0.3%
    uint256 discount = baseFee * (volumeDiscount + ageDiscount) / 100;
    
    return baseFee - discount;
}

Resolution Fees

Charge fees on winnings:

function claimWinnings(uint256 marketId) external {
    require(markets[marketId].resolved, "Not resolved");
    
    uint256 winnings = calculateWinnings(msg.sender, marketId);
    uint256 fee = (winnings * resolutionFeeBps) / 10000;
    uint256 payout = winnings - fee;
    
    // Send fee
    collateralToken.transfer(feeRecipient, fee);
    
    // Send winnings
    collateralToken.transfer(msg.sender, payout);
}

Market Resolution Mechanisms

Time-Based Resolution

Markets resolve at predetermined times:

function resolveMarket(
    uint256 marketId,
    uint256 outcome
) external {
    Market storage market = markets[marketId];
    require(block.timestamp >= market.resolutionTime, "Too early");
    require(!market.resolved, "Already resolved");
    require(msg.sender == market.oracle, "Not oracle");
    
    market.resolved = true;
    market.outcome = outcome;
    
    emit MarketResolved(marketId, outcome);
}

Early Resolution

Allow early resolution if outcome is certain:

function resolveEarly(
    uint256 marketId,
    uint256 outcome,
    bytes calldata proof
) external {
    require(_verifyEarlyResolutionProof(proof), "Invalid proof");
    
    Market storage market = markets[marketId];
    market.resolved = true;
    market.outcome = outcome;
    market.earlyResolution = true;
    
    // Return remaining time value to traders
    _distributeTimeValue(marketId);
    
    emit MarketResolvedEarly(marketId, outcome);
}

Invalid Market Resolution

Handle ambiguous or invalid markets:

function invalidateMarket(uint256 marketId) external {
    require(msg.sender == disputeResolver, "Not authorized");
    
    Market storage market = markets[marketId];
    market.invalid = true;
    market.resolved = true;
    
    // Return all collateral to traders
    _returnAllCollateral(marketId);
    
    emit MarketInvalidated(marketId);
}

Liquidity Provision

Concentrated Liquidity

contract ConcentratedLiquidityPM {
    struct LiquidityPosition {
        uint256 lowerPrice;
        uint256 upperPrice;
        uint256 liquidityAmount;
        address provider;
    }
    
    function addLiquidity(
        uint256 marketId,
        uint256 lowerPrice,
        uint256 upperPrice,
        uint256 amount
    ) external returns (uint256 positionId) {
        require(upperPrice > lowerPrice, "Invalid range");
        require(upperPrice <= 1e18, "Above max price");
        
        // Transfer both YES and NO tokens
        uint256 yesAmount = (amount * upperPrice) / 1e18;
        uint256 noAmount = amount - yesAmount;
        
        yesToken.transferFrom(msg.sender, address(this), yesAmount);
        noToken.transferFrom(msg.sender, address(this), noAmount);
        
        positionId = positionCount++;
        positions[positionId] = LiquidityPosition({
            lowerPrice: lowerPrice,
            upperPrice: upperPrice,
            liquidityAmount: amount,
            provider: msg.sender
        });
        
        emit LiquidityAdded(positionId, msg.sender, amount);
    }
}

Liquidity Mining

Incentivize liquidity provision:

contract LiquidityMiningPM {
    function calculateLPRewards(
        address lp,
        uint256 marketId
    ) public view returns (uint256) {
        uint256 timeProviding = block.timestamp - lpStartTime[lp][marketId];
        uint256 liquidityProvided = lpLiquidity[lp][marketId];
        uint256 marketVolume = getMarketVolume(marketId);
        
        // Rewards based on liquidity, time, and market activity
        return (liquidityProvided * timeProviding * marketVolume) / REWARD_DIVISOR;
    }
}

Risk Management

Maximum Exposure Limits

contract RiskManagement {
    mapping(uint256 => uint256) public maxMarketExposure;
    mapping(uint256 => uint256) public currentExposure;
    
    function setMaxExposure(uint256 marketId, uint256 maxAmount) external {
        require(msg.sender == riskManager, "Not authorized");
        maxMarketExposure[marketId] = maxAmount;
    }
    
    modifier withinExposureLimits(uint256 marketId, uint256 amount) {
        require(
            currentExposure[marketId] + amount <= maxMarketExposure[marketId],
            "Exceeds exposure limit"
        );
        _;
    }
}

Circuit Breakers

modifier notPaused(uint256 marketId) {
    require(!markets[marketId].paused, "Market paused");
    _;
}

function emergencyPause(uint256 marketId) external {
    require(msg.sender == guardian, "Not guardian");
    markets[marketId].paused = true;
    emit EmergencyPause(marketId);
}

Best Practices

1. Clear Resolution Criteria

Define exactly how markets resolve:

struct ResolutionCriteria {
    string primarySource; // e.g., "CNN", "Bloomberg"
    string backupSource; // In case primary fails
    uint256 resolutionDelay; // Time after event to resolve
    bool requiresManualVerification;
    string resolutionDescription; // Detailed explanation
}

2. Oracle Diversity

Don't rely on single oracles:

struct OracleConfig {
    address[] oracles;
    uint256 requiredConfirmations;
    uint256 disputePeriod;
}

3. Gradual Market Launch

Start with smaller markets, scale up:

uint256 public maxMarketSize = 100000e18; // Start small

function increaseMarketSize(uint256 newSize) external {
    require(msg.sender == governance, "Not governance");
    require(newSize > maxMarketSize, "Must increase");
    maxMarketSize = newSize;
}

4. Transparent Fee Structures

All fees visible and reasonable:

  • Trading fees: 0.1-0.5%
  • Resolution fees: 0-1%
  • Withdrawal fees: None or minimal

Conclusion

Building prediction markets requires balancing mathematical precision with user experience. The mechanics must be sound—AMM formulas, resolution logic, fee structures—but equally important is making markets accessible and engaging.

Successful prediction market platforms:

  1. Provide liquidity: Ensure users can trade without excessive slippage
  2. Resolve fairly: Build trust through transparent, accurate resolution
  3. Minimize fees: Keep costs low to encourage participation
  4. Manage risk: Protect against manipulation and systemic failure
  5. Iterate quickly: Learn from market data and user feedback

The prediction markets that thrive will be those that aggregate intelligence most effectively—creating environments where informed traders profit, markets reflect true probabilities, and the collective wisdom emerges through price discovery.