# Prediction Market

> Binary prediction market with YES/NO outcome tokens, liquidity pool trading, and vote-based settlement

**Category:** Token Types

## Summary

Required standards: \["Prediction Market"]

* Binary prediction market: "Will X happen by Y?" Users deposit USDC to mint paired YES+NO tokens. Trade YES↔NO on a liquidity pool. Verifier settles by voting. Winner redeems 1:1.
* Token ID 1 = YES, Token ID 2 = NO (via alias paths with 6 decimals)
* mintEscrowAddress holds all deposited USDC
* invariants: \`noForcefulPostMintTransfers: true\` — locks non-mint approvals (redeem, settlement, transferable) from using \`overridesFromOutgoingApprovals\` or \`overridesToIncomingApprovals\`. Non-mint approvals rely on \`defaultBalances.autoApproveSelfInitiatedOutgoingTransfers: true\` for outgoing-side auth and on the burn destination for incoming-side auth
* All permissions frozen after creation
* 7 approvals: paired mint, freely transferable, pre-settlement redeem, yes-wins, no-wins, push-yes, push-no
* Alias paths for YES (token 1) and NO (token 2) with 6 decimals
* Settlement via votingChallenges with 1-of-1 multisig verifier
* Liquidity pool: MsgCreateBalancerPool with badgeslp:collectionId:uyes and badgeslp:collectionId:uno, equal weights
* DON'T use Smart Token standard — this uses mintEscrowAddress, not invariant paths
* DON'T forget votingChallenges on settlement approvals
* DON'T set maxNumTransfers on mint/redeem (should be unlimited = 0)
* DON'T forget to freeze all permissions
* DON'T forget predeterminedBalances with BOTH token IDs in paired mint/redeem
* DON'T set overrideFromWithApproverAddress on the deposit coinTransfer (filler pays, not escrow)

## Instructions

## Prediction Market Configuration

### Mental Model

Binary prediction market: "Will X happen by Y?" Users deposit USDC to mint paired YES+NO tokens. Trade YES↔NO on a liquidity pool. Verifier settles by voting. Winner redeems 1:1.

### Collection Structure

* Token ID 1 = YES, Token ID 2 = NO (via alias paths with 6 decimals)
* Standard: 'Prediction Market'
* mintEscrowAddress holds all deposited USDC
* All permissions frozen after creation

### Alias Paths

Two alias paths are REQUIRED — one for YES (token 1) and one for NO (token 2):

```json
[
  {
    "denom": "uyes",
    "symbol": "uyes",
    "conversion": {
      "sideA": { "amount": "1" },
      "sideB": [{ "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }]
    },
    "denomUnits": [{ "symbol": "YES", "decimals": "6", "isDefaultDisplay": true }]
  },
  {
    "denom": "uno",
    "symbol": "uno",
    "conversion": {
      "sideA": { "amount": "1" },
      "sideB": [{ "amount": "1", "tokenIds": [{ "start": "2", "end": "2" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }]
    },
    "denomUnits": [{ "symbol": "NO", "decimals": "6", "isDefaultDisplay": true }]
  }
]
```

### 7 Approvals

**CRITICAL: All amounts in startBalances must be in BASE units (micro-units). Since YES/NO tokens have 6 decimals, 1 display token = 1,000,000 base units. Use "1000000" (not "1") for startBalance amounts when minting 1 display-YES + 1 display-NO per deposit.**

#### 1. Paired Mint (deposit 1 USDC → receive 1 YES + 1 NO)

```json
{
  "approvalId": "paired-mint",
  "fromListId": "Mint",
  "toListId": "All",
  "initiatedByListId": "All",
  "tokenIds": [{ "start": "1", "end": "2" }],
  "approvalCriteria": {
    "overridesFromOutgoingApprovals": true,
    "predeterminedBalances": {
      "manualBalances": [],
      "incrementedBalances": {
        "startBalances": [
          { "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] },
          { "amount": "1", "tokenIds": [{ "start": "2", "end": "2" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }
        ],
        "incrementTokenIdsBy": "0",
        "incrementOwnershipTimesBy": "0",
        "durationFromTimestamp": "0",
        "allowOverrideTimestamp": false,
        "recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" }
      },
      "orderCalculationMethod": {
        "useOverallNumTransfers": true,
        "usePerToAddressNumTransfers": false,
        "usePerFromAddressNumTransfers": false,
        "usePerInitiatedByAddressNumTransfers": false,
        "useMerkleChallengeLeafIndex": false,
        "challengeTrackerId": ""
      }
    },
    "coinTransfers": [{
      "to": "Mint",
      "overrideFromWithApproverAddress": false,
      "overrideToWithInitiator": false,
      "coins": [{ "amount": "1000000", "denom": "<USDC_IBC_DENOM>" }]
    }]
  }
}
```

> **CRITICAL:** The `to` field MUST be `"Mint"`. The chain auto-resolves `"Mint"` to the collection's `mintEscrowAddress` at execution time for collection-level approvals. This ensures deposited USDC goes to the escrow (not the creator's wallet) and is available for redemption payouts.
>
> DO NOT use the creator's address or any hardcoded address — use `"Mint"` which auto-resolves to the escrow.

Note: The deposit coinTransfer uses `to: "Mint"` with no overrides: initiator pays USDC → escrow (auto-resolved). The payout coinTransfers use `to: ""` with `overrideFromWithApproverAddress: true` (escrow pays) + `overrideToWithInitiator: true` (redeemer receives).

#### 2. Freely Transferable (allows transfers between users, pools, DEX)

```json
{
  "approvalId": "transferable",
  "fromListId": "!Mint",
  "toListId": "All",
  "initiatedByListId": "All",
  "tokenIds": [{ "start": "1", "end": "2" }],
  "approvalCriteria": {
    "overridesFromOutgoingApprovals": false,
    "overridesToIncomingApprovals": false,
    "mustPrioritize": false
  }
}
```

> This approval has NO coinTransfers, NO votingChallenges, and mustPrioritize: false. It allows auto-scanning so tokens can be freely transferred to pool addresses and between users.

#### 3. Pre-Settlement Redeem (burn 1 YES + 1 NO → 1 USDC from escrow)

```json
{
  "approvalId": "pre-settlement-redeem",
  "fromListId": "!Mint",
  "toListId": "<BURN_ADDRESS>",
  "initiatedByListId": "All",
  "tokenIds": [{ "start": "1", "end": "2" }],
  "approvalCriteria": {
    "predeterminedBalances": {
      "manualBalances": [],
      "incrementedBalances": {
        "startBalances": [
          { "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] },
          { "amount": "1", "tokenIds": [{ "start": "2", "end": "2" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }
        ],
        "incrementTokenIdsBy": "0",
        "incrementOwnershipTimesBy": "0",
        "durationFromTimestamp": "0",
        "allowOverrideTimestamp": false,
        "recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" }
      },
      "orderCalculationMethod": {
        "useOverallNumTransfers": true,
        "usePerToAddressNumTransfers": false,
        "usePerFromAddressNumTransfers": false,
        "usePerInitiatedByAddressNumTransfers": false,
        "useMerkleChallengeLeafIndex": false,
        "challengeTrackerId": ""
      }
    },
    "coinTransfers": [{
      "to": "",
      "overrideFromWithApproverAddress": true,
      "overrideToWithInitiator": true,
      "coins": [{ "amount": "1000000", "denom": "<USDC_IBC_DENOM>" }]
    }],
    "maxNumTransfers": {
      "overallMaxNumTransfers": "18446744073709551615",
      "perFromAddressMaxNumTransfers": "0",
      "perToAddressMaxNumTransfers": "0",
      "perInitiatedByAddressMaxNumTransfers": "0",
      "amountTrackerId": "pre-settlement-redeem",
      "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
    }
  }
}
```

#### 4. YES Wins (burn YES → 1 USDC)

```json
{
  "approvalId": "yes-wins",
  "fromListId": "!Mint",
  "toListId": "<BURN_ADDRESS>",
  "initiatedByListId": "All",
  "tokenIds": [{ "start": "1", "end": "1" }],
  "approvalCriteria": {
    "predeterminedBalances": {
      "manualBalances": [],
      "incrementedBalances": {
        "startBalances": [
          { "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }
        ],
        "incrementTokenIdsBy": "0",
        "incrementOwnershipTimesBy": "0",
        "durationFromTimestamp": "0",
        "allowOverrideTimestamp": false,
        "recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" }
      },
      "orderCalculationMethod": {
        "useOverallNumTransfers": true,
        "usePerToAddressNumTransfers": false,
        "usePerFromAddressNumTransfers": false,
        "usePerInitiatedByAddressNumTransfers": false,
        "useMerkleChallengeLeafIndex": false,
        "challengeTrackerId": ""
      }
    },
    "coinTransfers": [{
      "to": "",
      "overrideFromWithApproverAddress": true,
      "overrideToWithInitiator": true,
      "coins": [{ "amount": "1000000", "denom": "<USDC_IBC_DENOM>" }]
    }],
    "maxNumTransfers": {
      "overallMaxNumTransfers": "18446744073709551615",
      "perFromAddressMaxNumTransfers": "0",
      "perToAddressMaxNumTransfers": "0",
      "perInitiatedByAddressMaxNumTransfers": "0",
      "amountTrackerId": "yes-wins",
      "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
    },
    "votingChallenges": [{
      "proposalId": "yes-wins-proposal",
      "quorumThreshold": "100",
      "voters": [{ "address": "<VERIFIER_ADDRESS>", "weight": "1" }]
    }]
  }
}
```

#### 5. NO Wins (burn NO → 1 USDC)

Same as YES Wins but with token ID 2, separate proposalId, and separate amountTrackerId:

```json
{
  "approvalId": "no-wins",
  "fromListId": "!Mint",
  "toListId": "<BURN_ADDRESS>",
  "initiatedByListId": "All",
  "tokenIds": [{ "start": "2", "end": "2" }],
  "approvalCriteria": {
    "predeterminedBalances": {
      "manualBalances": [],
      "incrementedBalances": {
        "startBalances": [
          { "amount": "1", "tokenIds": [{ "start": "2", "end": "2" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }
        ],
        "incrementTokenIdsBy": "0",
        "incrementOwnershipTimesBy": "0",
        "durationFromTimestamp": "0",
        "allowOverrideTimestamp": false,
        "recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" }
      },
      "orderCalculationMethod": {
        "useOverallNumTransfers": true,
        "usePerToAddressNumTransfers": false,
        "usePerFromAddressNumTransfers": false,
        "usePerInitiatedByAddressNumTransfers": false,
        "useMerkleChallengeLeafIndex": false,
        "challengeTrackerId": ""
      }
    },
    "coinTransfers": [{
      "to": "",
      "overrideFromWithApproverAddress": true,
      "overrideToWithInitiator": true,
      "coins": [{ "amount": "1000000", "denom": "<USDC_IBC_DENOM>" }]
    }],
    "maxNumTransfers": {
      "overallMaxNumTransfers": "18446744073709551615",
      "perFromAddressMaxNumTransfers": "0",
      "perToAddressMaxNumTransfers": "0",
      "perInitiatedByAddressMaxNumTransfers": "0",
      "amountTrackerId": "no-wins",
      "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
    },
    "votingChallenges": [{
      "proposalId": "no-wins-proposal",
      "quorumThreshold": "100",
      "voters": [{ "address": "<VERIFIER_ADDRESS>", "weight": "1" }]
    }]
  }
}
```

#### 6. Push YES (burn YES → 0.5 USDC — fallback if market is indeterminate)

```json
{
  "approvalId": "push-yes",
  "fromListId": "!Mint",
  "toListId": "<BURN_ADDRESS>",
  "initiatedByListId": "All",
  "tokenIds": [{ "start": "1", "end": "1" }],
  "approvalCriteria": {
    "predeterminedBalances": {
      "manualBalances": [],
      "incrementedBalances": {
        "startBalances": [
          { "amount": "1", "tokenIds": [{ "start": "1", "end": "1" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }
        ],
        "incrementTokenIdsBy": "0",
        "incrementOwnershipTimesBy": "0",
        "durationFromTimestamp": "0",
        "allowOverrideTimestamp": false,
        "recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" }
      },
      "orderCalculationMethod": {
        "useOverallNumTransfers": true,
        "usePerToAddressNumTransfers": false,
        "usePerFromAddressNumTransfers": false,
        "usePerInitiatedByAddressNumTransfers": false,
        "useMerkleChallengeLeafIndex": false,
        "challengeTrackerId": ""
      }
    },
    "coinTransfers": [{
      "to": "",
      "overrideFromWithApproverAddress": true,
      "overrideToWithInitiator": true,
      "coins": [{ "amount": "500000", "denom": "<USDC_IBC_DENOM>" }]
    }],
    "maxNumTransfers": {
      "overallMaxNumTransfers": "18446744073709551615",
      "perFromAddressMaxNumTransfers": "0",
      "perToAddressMaxNumTransfers": "0",
      "perInitiatedByAddressMaxNumTransfers": "0",
      "amountTrackerId": "push-yes",
      "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
    },
    "votingChallenges": [{
      "proposalId": "push-yes-proposal",
      "quorumThreshold": "100",
      "voters": [{ "address": "<VERIFIER_ADDRESS>", "weight": "1" }]
    }]
  }
}
```

#### 7. Push NO (burn NO → 0.5 USDC — fallback if market is indeterminate)

Same as Push YES but with token ID 2 and a separate proposalId:

```json
{
  "approvalId": "push-no",
  "fromListId": "!Mint",
  "toListId": "<BURN_ADDRESS>",
  "initiatedByListId": "All",
  "tokenIds": [{ "start": "2", "end": "2" }],
  "approvalCriteria": {
    "predeterminedBalances": {
      "manualBalances": [],
      "incrementedBalances": {
        "startBalances": [
          { "amount": "1", "tokenIds": [{ "start": "2", "end": "2" }], "ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }] }
        ],
        "incrementTokenIdsBy": "0",
        "incrementOwnershipTimesBy": "0",
        "durationFromTimestamp": "0",
        "allowOverrideTimestamp": false,
        "recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" }
      },
      "orderCalculationMethod": {
        "useOverallNumTransfers": true,
        "usePerToAddressNumTransfers": false,
        "usePerFromAddressNumTransfers": false,
        "usePerInitiatedByAddressNumTransfers": false,
        "useMerkleChallengeLeafIndex": false,
        "challengeTrackerId": ""
      }
    },
    "coinTransfers": [{
      "to": "",
      "overrideFromWithApproverAddress": true,
      "overrideToWithInitiator": true,
      "coins": [{ "amount": "500000", "denom": "<USDC_IBC_DENOM>" }]
    }],
    "maxNumTransfers": {
      "overallMaxNumTransfers": "18446744073709551615",
      "perFromAddressMaxNumTransfers": "0",
      "perToAddressMaxNumTransfers": "0",
      "perInitiatedByAddressMaxNumTransfers": "0",
      "amountTrackerId": "push-no",
      "resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
    },
    "votingChallenges": [{
      "proposalId": "push-no-proposal",
      "quorumThreshold": "100",
      "voters": [{ "address": "<VERIFIER_ADDRESS>", "weight": "1" }]
    }]
  }
}
```

### Settlement Flow

1. Verifier sends MsgCastVote with 100% yes on the proposalId of the winning approval
2. Once quorum reached, that approval becomes active for transfers
3. Holders burn winning token to receive USDC from escrow

### Liquidity Pool

After creating the collection and minting initial pairs:

* Create pool: MsgCreateBalancerPool with badgeslp:collectionId:uyes and badgeslp:collectionId:uno, equal weights
* Market price discovery: YES\_price = NO\_reserve / (YES\_reserve + NO\_reserve)

### Steps for AI Builder

1. Use per-field tools to initialize the collection (set\_standards, set\_valid\_token\_ids, etc.)
2. `set_token_metadata` for YES (token 1) and NO (token 2)
3. `set_invariants` with `{ "noCustomOwnershipTimes": true, "disablePoolCreation": false }` — MUST set disablePoolCreation to false
4. Add 7 approvals via `add_approval`:
   * `paired-mint`: Mint → All (deposit USDC, receive YES+NO). coinTransfer `to` MUST be `"Mint"` — the chain auto-resolves this to the collection's mintEscrowAddress at execution time. No overrides on deposit.
   * `transferable`: !Mint → All (free transfers between users/pools, NO coinTransfers, NO mustPrioritize, overridesFromOutgoingApprovals: false, overridesToIncomingApprovals: false)
   * `pre-settlement-redeem`: !Mint → burn (redeem pair for USDC). coinTransfer `to: ""` with `overrideFromWithApproverAddress: true` + `overrideToWithInitiator: true` (escrow pays redeemer).
   * `yes-wins`, `no-wins`, `push-yes`, `push-no`: settlement (vote-gated). Same payout pattern: `to: ""` with both overrides true.
5. `set_mint_escrow_coins` — NOT needed upfront (coins come from deposits)
6. Add alias paths via `add_alias_path` for YES and NO
7. `set_permissions` with preset `"fully-immutable"` to freeze everything (NOT "locked-approvals" — that leaves some permissions neutral)
8. After collection creation: mint initial pairs + create pool

### Common Mistakes

* DON'T forget both alias paths (uyes and uno)
* DON'T set the alias denom/symbol the same as the denomUnit symbol — the chain rejects duplicate denom unit symbols. Use base denoms like "uyes"/"uno" with display denomUnits "YES"/"NO" (6 decimals)
* DON'T use Smart Token standard — this uses mintEscrowAddress, not invariant paths
* DON'T forget votingChallenges on settlement approvals
* DON'T set maxNumTransfers on the paired mint approval (deposits should be unlimited = 0)
* DO set maxNumTransfers.overallMaxNumTransfers to "18446744073709551615" (max uint64) on ALL approvals that use overrideFromWithApproverAddress: true (redeem + settlement). The chain REQUIRES a non-zero maxNumTransfers when overrideFromWithApproverAddress is true. Use max uint64 for effectively unlimited.
* DON'T disable overrideFromWithApproverAddress to work around maxNumTransfers errors — that breaks payout routing. Always keep overrideFromWithApproverAddress: true on redemption/settlement approvals and set maxNumTransfers to max uint64.
* DON'T forget to freeze all permissions
* DON'T forget predeterminedBalances with BOTH token IDs in paired mint/redeem
* DON'T set overrideFromWithApproverAddress on the deposit coinTransfer (filler pays, not escrow)
* DON'T leave the "to" field empty on the paired mint coinTransfer — use `"Mint"` which auto-resolves to the collection's mintEscrowAddress at execution time.
* DON'T use the creator's address or any hardcoded address as "to" on the deposit — use `"Mint"` for auto-routing to escrow.
* DON'T hardcode the creator address as the coinTransfer "to" on redemption/settlement — use overrideToWithInitiator: true so the person redeeming receives the payout
* DON'T use "1" for startBalance amounts — YES/NO tokens have 6 decimals, so 1 display token = 1,000,000 base units. Use "1000000" for each startBalance amount.
* DON'T use `set_permissions` preset "locked-approvals" — use "fully-immutable" to freeze ALL permissions including validTokenIds
* DON'T use lowercase "prediction-market" as the standard — the correct name is "Prediction Market" (title case with space)
* DON'T set invariants.disablePoolCreation to true — prediction markets REQUIRE liquidity pools for YES/NO trading. Set it to false.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.bitbadges.io/token-standard/skills/prediction-market.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
