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.
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
| Feature | Order Book | AMM |
|---|---|---|
| Liquidity | Requires makers | Algorithmic |
| Price discovery | Efficient with depth | Formula-based |
| MEV exposure | High | Moderate |
| Gas costs | Lower for makers | Predictable |
| Scalability | Fragmented by market | Unified pools |
| Best for | Large markets | Long-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:
- Provide liquidity: Ensure users can trade without excessive slippage
- Resolve fairly: Build trust through transparent, accurate resolution
- Minimize fees: Keep costs low to encourage participation
- Manage risk: Protect against manipulation and systemic failure
- 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.