Skip to main content
Every Proteus market follows a deterministic lifecycle with clearly defined states and transitions. Understanding this flow is essential for participants and developers.

Lifecycle Overview

State Definitions

1. Created

Market Created

Trigger: createMarket(actorHandle, duration)State: Market exists but may have no submissions yetConstraints:
  • Duration must be between 1 hour and 30 days
  • Actor handle specified
  • Pool starts at 0 ETH
PredictionMarketV2.sol:139-159
function createMarket(
    string calldata _actorHandle,
    uint256 _duration
) external whenNotPaused returns (uint256) {
    if (_duration < 1 hours || _duration > 30 days) revert InvalidDuration();
    
    uint256 marketId = marketCount++;
    
    markets[marketId] = Market({
        actorHandle: _actorHandle,
        endTime: block.timestamp + _duration,
        totalPool: 0,
        resolved: false,
        winningSubmissionId: 0,
        creator: msg.sender
    });
    
    emit MarketCreated(marketId, _actorHandle, block.timestamp + _duration, msg.sender);
    
    return marketId;
}
uint256 public constant MIN_BET = 0.001 ether;
uint256 public constant BETTING_CUTOFF = 1 hours;
uint256 public constant MIN_SUBMISSIONS = 2;
uint256 public constant MAX_TEXT_LENGTH = 280;

2. Active (Accepting Submissions)

Active Market

Condition: block.timestamp < endTime - BETTING_CUTOFFActions Allowed:
  • Submit predictions with ETH stake
  • View current submissions and pool size
Actions Prohibited:
  • Cannot resolve market
  • Cannot claim payouts
PredictionMarketV2.sol:167-198
function createSubmission(
    uint256 _marketId,
    string calldata _predictedText
) external payable whenNotPaused nonReentrant returns (uint256) {
    Market storage market = markets[_marketId];
    
    if (market.endTime == 0) revert MarketNotFound();
    if (market.resolved) revert MarketAlreadyResolved();
    if (block.timestamp >= market.endTime) revert MarketEnded();
    if (block.timestamp >= market.endTime - BETTING_CUTOFF) revert BettingCutoffPassed();
    if (msg.value < MIN_BET) revert InsufficientBet();
    if (bytes(_predictedText).length == 0) revert EmptyPrediction();
    if (bytes(_predictedText).length > MAX_TEXT_LENGTH) revert PredictionTooLong();
    
    uint256 submissionId = submissionCount++;
    
    submissions[submissionId] = Submission({
        marketId: _marketId,
        submitter: msg.sender,
        predictedText: _predictedText,
        amount: msg.value,
        claimed: false
    });
    
    marketSubmissions[_marketId].push(submissionId);
    userSubmissions[msg.sender].push(submissionId);
    market.totalPool += msg.value;
    
    emit SubmissionCreated(submissionId, _marketId, msg.sender, _predictedText, msg.value);
    
    return submissionId;
}

Submission Validation

1

Market Exists

endTime != 0 (market was created)
2

Not Yet Resolved

resolved == false
3

Before Market End

block.timestamp < endTime
4

Before Betting Cutoff

block.timestamp < endTime - 1 hour
5

Minimum Stake

msg.value >= 0.001 ETH
6

Valid Text

Not empty, ≤ 280 characters
Betting Cutoff: The 1-hour cutoff before market end prevents last-second front-running when the resolution text may already be known.

3. Betting Closed

Betting Closed

Condition: endTime - BETTING_CUTOFF ≤ block.timestamp < endTimeDuration: Final 1 hour before market endState: No new submissions accepted, waiting for market to end
This is a “cooling off” period. The actual post may already be published, but oracles need time to verify and submit the resolution.

4. Market Ended (Awaiting Resolution)

Market Ended

Condition: block.timestamp >= endTime && !resolvedNext Steps:
  • Oracle fetches actual text from X API
  • Owner calls resolveMarket() with actual text
  • OR if only 1 submission exists: anyone can call refundSingleSubmission()
PredictionMarketV2.sol:207-244
function resolveMarket(
    uint256 _marketId,
    string calldata _actualText
) external onlyOwner {
    Market storage market = markets[_marketId];
    
    if (market.endTime == 0) revert MarketNotFound();
    if (market.resolved) revert MarketAlreadyResolved();
    if (block.timestamp < market.endTime) revert MarketNotEnded();
    
    uint256[] storage subIds = marketSubmissions[_marketId];
    if (subIds.length < MIN_SUBMISSIONS) revert MinimumSubmissionsNotMet();
    
    // Find submission with minimum Levenshtein distance
    uint256 winningId = subIds[0];
    uint256 minDistance = levenshteinDistance(
        submissions[subIds[0]].predictedText,
        _actualText
    );
    
    for (uint256 i = 1; i < subIds.length; i++) {
        uint256 distance = levenshteinDistance(
            submissions[subIds[i]].predictedText,
            _actualText
        );
        // Strict less-than: first submitter wins ties
        if (distance < minDistance) {
            minDistance = distance;
            winningId = subIds[i];
        }
    }
    
    market.resolved = true;
    market.winningSubmissionId = winningId;
    
    emit MarketResolved(_marketId, winningId, _actualText, minDistance);
}

5. Resolved

Market Resolved

Condition: resolved == true && winningSubmissionId != 0State:
  • Winner determined by minimum Levenshtein distance
  • First submitter wins ties (deterministic)
  • Pool ready for winner to claim

Winner Determination Logic

1

Calculate All Distances

For each submission, compute d_L(predictedText, actualText) on-chain
2

Find Minimum

winningId = argmin_{submissions} d_L
3

Break Ties

Use strict less-than (distance < minDistance), so first submitter wins ties
4

Store Result

Set market.winningSubmissionId
This creates an incentive to submit early and confidently. Waiting to see others’ submissions provides no advantage because:
  1. All submissions are committed on-chain (can’t change after submitting)
  2. Comparison is against actual text, not other submissions
  3. Early submission shows conviction
The tie-breaking rule is deterministic and documented, ensuring fairness.

6. Claimed

Payout Claimed

Trigger: Winner calls claimPayout(submissionId)Final State: ETH transferred to winner, fees accumulated for withdrawal
PredictionMarketV2.sol:250-273
function claimPayout(uint256 _submissionId) external nonReentrant {
    Submission storage submission = submissions[_submissionId];
    Market storage market = markets[submission.marketId];
    
    if (submission.submitter == address(0)) revert SubmissionNotFound();
    if (!market.resolved) revert MarketNotEnded();
    if (market.winningSubmissionId != _submissionId) revert NotWinningSubmission();
    if (submission.claimed) revert AlreadyClaimed();
    
    submission.claimed = true;
    
    uint256 totalPool = market.totalPool;
    uint256 fee = (totalPool * PLATFORM_FEE_BPS) / 10000;  // 7%
    uint256 payout = totalPool - fee;
    
    // Accumulate fees for pull-based withdrawal
    pendingFees[feeRecipient] += fee;
    
    // Transfer payout to winner
    (bool success, ) = submission.submitter.call{value: payout}("");
    if (!success) revert TransferFailed();
    
    emit PayoutClaimed(_submissionId, submission.submitter, payout);
}

Payout Calculation

fee = floor(totalPool × 700 / 10000)  // 7% = 700 basis points
payout = totalPool - fee               // 93% to winner
Pull-based fees: Fees accumulate in pendingFees[feeRecipient] and are withdrawn separately. This prevents griefing attacks where a malicious fee recipient reverts transfers.

Special Cases

The NULL Sentinel

Predicting Silence

Markets can resolve with __NULL__ if the target doesn’t post.Use Case: Betting that a public figure will not post during the market window
Prediction: __NULL__
Actual: __NULL__ (no post)

Levenshtein distance: 0 (exact match)
Result: Perfect prediction, winner takes entire pool
From Example 4 in the whitepaper:
SubmitterPredictionDistance
Null trader__NULL__0
Human (guessing)“Jensen will flex about Blackwell…“46
AI Roleplay”NVIDIA Blackwell Ultra is sampling…“90
Winner: Null trader at distance 0 (exact match)
AI roleplay agents always generate text - they cannot predict silence. The __NULL__ sentinel enables a market primitive that binary contracts cannot express.

Emergency Withdrawal

Admin Safety Valve

Trigger: Owner only, 7+ days after market end, market not resolvedAction: Refund all participants, mark market as resolvedPurpose: Recover funds if oracle fails or resolution becomes impossible
PredictionMarketV2.sol:425-447
function emergencyWithdraw(uint256 _marketId) external onlyOwner nonReentrant {
    Market storage market = markets[_marketId];
    
    if (market.endTime == 0) revert MarketNotFound();
    if (market.resolved) revert MarketAlreadyResolved();
    // Only allow 7 days after market end
    if (block.timestamp < market.endTime + 7 days) revert MarketNotEnded();
    
    market.resolved = true;
    
    uint256[] storage subIds = marketSubmissions[_marketId];
    for (uint256 i = 0; i < subIds.length; i++) {
        Submission storage sub = submissions[subIds[i]];
        if (!sub.claimed && sub.amount > 0) {
            sub.claimed = true;
            (bool success, ) = sub.submitter.call{value: sub.amount}("");
            // Continue even if transfer fails
            if (success) {
                emit SingleSubmissionRefunded(_marketId, subIds[i], sub.submitter, sub.amount);
            }
        }
    }
}

State Diagram Details

Time-Based Transitions

Guard Conditions

Each state transition has strict guard conditions:
require(_duration >= 1 hours && _duration <= 30 days);
require(!paused);

Events Emitted

Every state transition emits events for off-chain tracking:
event MarketCreated(
    uint256 indexed marketId,
    string actorHandle,
    uint256 endTime,
    address creator
);

View Functions

Query market state at any time:

getMarketDetails()

Returns:
  • Actor handle
  • End time
  • Total pool
  • Resolved status
  • Winning submission ID
  • Creator
  • All submission IDs

getSubmissionDetails()

Returns:
  • Market ID
  • Submitter address
  • Predicted text
  • Amount staked
  • Claimed status

getMarketSubmissions()

Returns array of all submission IDs for a market

getUserSubmissions()

Returns array of all submission IDs for a user

Key Takeaways

Deterministic: Every state transition has clear conditions and guard rails
Fair: First submitter wins ties, betting cutoff prevents front-running
Safe: Pull-based fees, reentrancy guards, emergency withdrawal
Transparent: All state changes emit events for off-chain tracking

Next Steps

Fee Structure

Learn how the 7% platform fee is distributed

Levenshtein Distance

Deep dive into the scoring mechanism

Build docs developers (and LLMs) love