- Yellow Demo: Watch Video
- ENS Demo: Watch Video
One-liner: A combined prediction market that pools liquidity across multiple correlated Yes/No questions by pricing them through a single joint-outcome (“world table”) AMM, while still letting users trade simple binary markets and “partial” multi-event slices.
Prediction markets often fragment liquidity across many correlated questions (e.g., related geopolitics events), causing wide spreads and inconsistent probabilities.
This project proposes a single shared market-making engine over the joint outcomes of multiple binary events, so:
- One liquidity pool supports all correlated questions.
- Prices remain coherent (no contradictory implied probabilities).
- Users can trade:
- Simple binary bets (e.g., “Event A = Yes”)
- Partial multi-event bets (e.g., “A = Yes AND B = Yes, regardless of C”)
- Exact scenario bets (e.g., “A=Yes, B=Yes, C=No”)
All contracts still resolve to $1 per share if they win, $0 otherwise. “Bigger upside” comes from buying more shares at lower prices (more specific scenarios are cheaper).
In typical prediction markets, each question is its own market / book:
- Market A: P(A=Yes)
- Market B: P(B=Yes)
- Market C: P(C=Yes)
Even if these events are strongly correlated, liquidity is split across separate pools/orderbooks, leading to:
- Wider spreads (less depth per market)
- Slower price discovery
- Incoherence (prices don’t move together unless arbitrage traders manually sync them)
Separate markets can imply contradictory “stories of the world.” Example: “Israel strike” jumps to 70%, but “US strike” stays flat even if historically/structurally correlated.
Instead of 3 separate markets, run one unified source of truth under the hood:
For N binary events, there are 2^N joint outcomes (“world states”).
For N=3 events A, B, C → 8 worlds:
| World (A,B,C) | Meaning |
|---|---|
| 000 | A no, B no, C no |
| 001 | A no, B no, C yes |
| 010 | A no, B yes, C no |
| 011 | A no, B yes, C yes |
| 100 | A yes, B no, C no |
| 101 | A yes, B no, C yes |
| 110 | A yes, B yes, C no |
| 111 | A yes, B yes, C yes |
The engine maintains probabilities/prices for each world:
p000, p001, ... p111- All non-negative
- Sum to 1 (probability simplex)
This world table is the single “source of truth.”
User-facing odds for individual questions are derived from the world table.
Example:
P(A=Yes) = p100 + p101 + p110 + p111P(B=Yes) = p010 + p011 + p110 + p111P(C=Yes) = p001 + p011 + p101 + p111
So users still see familiar Yes/No markets, but those are views of the one joint model.
All contracts pay $1 per share if the contract condition is satisfied, else $0.
A=Yes regardless of B,C
This is a 1D slice: it groups all worlds where A=1:
- {100,101,110,111}
Payout:
- $1 per share if A resolves Yes
- $0 otherwise
Price:
price(A=Yes) = P(A=Yes)(derived from world table)
A=Yes AND B=Yes, regardless of C
This is a 2D slice: it groups:
- {110,111}
Payout:
- $1 per share if (A=Yes AND B=Yes)
- $0 otherwise
Price:
price(A=Yes,B=Yes) = p110 + p111
Intuition:
- Slice is easier to win than an exact scenario (more worlds included),
- so it typically costs more per share.
A=Yes, B=Yes, C=No (world 110)
This is the most specific bet: one exact world.
Payout:
- $1 per share if the final world is exactly 110
- $0 otherwise
Price:
price(110) = p110
Intuition:
- Corner is harder to win (one exact outcome),
- so it’s usually cheaper per share.
Payout is always $1 per share.
Your total payout depends on how many shares you bought.
If you spend $1 USDC:
- Shares bought =
1 / price
Example:
- Corner price = $0.10 → you buy 10 shares → win pays $10
- Slice price = $0.20 → you buy 5 shares → win pays $5
So:
- Corner: cheaper → more shares → bigger payout if right, but lower hit-rate
- Slice: more expensive → fewer shares → smaller payout, but higher hit-rate
This naturally matches user intuition: precision = higher risk/higher reward.
Scaling payouts by 1/n (e.g., each event pays $1/3) mostly just changes units:
- users will buy 3x shares to get the same exposure
- it does not merge liquidity across markets
Liquidity pooling comes from one shared joint engine, not from payout scaling.
We replace the passive pool model with an Active Virtual AMM (vAMM) that acts as a "Robot Market Maker" on a Central Limit Order Book (CLOB).
Standard LMSR has a "static liquidity" bug. We upgrade to LS-LMSR:
| Feature | Description |
|---|---|
| Dynamic Depth | The liquidity parameter b grows with market volume: b = α × Volume |
| Benefit | The market deepens automatically. "Whales" can trade with lower slippage as the market matures |
The vAMM calculates a "Fair Price" and places Limit Orders. Users can undercut this price. The CLOB always fills the Best Price First.
Example: Trading "Event A" (vAMM fair value = $0.60)
| Rank | Price | Seller | Status (Who gets filled?) |
|---|---|---|---|
| 1st | $0.58 | User Steve | Executed First. (Best Price for Buyer) |
| 2nd | $0.60 | vAMM (Robot) | Executed Second. (Only if Steve runs out) |
| 3rd | $0.65 | User Alice | Executed Third. (Worst Price) |
- Buying: Lowest Price wins (User Steve > vAMM)
- Selling: Highest Price wins (vAMM > Lowballers)
The vAMM does not place one order for "Infinity Shares." It places a Ladder of orders to represent slippage visually:
Order 1: 500 shares @ $0.60
Order 2: 500 shares @ $0.61
Order 3: 500 shares @ $0.62
...
This allows the CLOB to function normally while accessing the vAMM's infinite depth.
When a user trades against the vAMM:
- User sends USDC
- vAMM adds its own subsidy (if needed) to complete the $1.00 collateral
- Gnosis CTF mints the Full Set (Outcomes A-H)
- vAMM gives the User their share (A) and keeps the rest (B-H) in its inventory
To prevent the vAMM from being drained in extreme conditions, we implement three defenses:
If the price moves too fast (e.g., jumps from $0.20 to $0.80 in minutes), the vAMM interprets this as "Uninformed/Toxic Flow."
- Action: Automatically widens the spread (e.g., Buy @ $0.30, Sell @ $0.70)
- Result: Traders must pay a higher fee to trade during panic, compensating LPs for the risk
If the vAMM holds too much of one outcome (e.g., Short "Yes", Long "No"):
- Action: Shifts prices to discourage buying Yes and encourage selling Yes
- Result: The market naturally re-balances the vAMM's inventory
We do not cap the number of shares. We cap the USDC Risk.
- Limit: The vAMM can only mint new shares as long as the LP Vault has funds to pay the subsidy
- Effect: If the vault hits $0, the vAMM stops quoting. This guarantees LPs cannot lose more than they deposited
Users typically trade marginals/slices, not raw corners.
Example: user buys C=Yes
- This corresponds to buying a basket of world outcomes where C=1:
- {001,011,101,111}
The vAMM processes this as a multi-outcome trade and updates prices across all 8 worlds. Because the world prices changed, the derived markets (A, B, slices) update too.
The LP seeds the market by depositing collateral (e.g., USDC) to back payouts and provide depth.
- LP deposits collateral into the AMM pool (e.g., $10,000 USDC).
- The AMM starts with an initial world-table prior (often uniform or mildly informed).
- As users trade, the AMM updates prices.
The LP does not need to “buy all tokens.” Collateral + AMM mechanics are enough to quote and settle.
Assume N=3. We maintain 8 probabilities that sum to 1:
| World | Prob |
|---|---|
| 000 | 0.20 |
| 001 | 0.05 |
| 010 | 0.15 |
| 011 | 0.10 |
| 100 | 0.10 |
| 101 | 0.05 |
| 110 | 0.25 |
| 111 | 0.10 |
| Sum = 1.00 |
P(A=Yes) = 100+101+110+111 = 0.10+0.05+0.25+0.10 = 0.50P(B=Yes) = 010+011+110+111 = 0.15+0.10+0.25+0.10 = 0.60P(C=Yes) = 001+011+101+111 = 0.05+0.10+0.05+0.10 = 0.30
- Corner (110) price = 0.25
- Slice (A=Yes,B=Yes regardless of C) price = 110+111 = 0.25+0.10 = 0.35
If user spends $1:
- Corner shares = 1/0.25 = 4 → payout $4 if 110 happens
- Slice shares = 1/0.35 ≈ 2.857 → payout ≈ $2.857 if 110 or 111 happens
- Trade familiar Yes/No markets with deeper liquidity
- Express richer views (scenarios, partial bets) with fewer steps
- Better pricing (tighter spreads, less slippage) due to pooled liquidity
- Coherent cross-market movement (related markets update together)
- One inventory/risk surface instead of fragmented books
- Cleaner hedging via mergeable/splittable exposures (corner ↔ slice ↔ marginal)
- Higher capital efficiency when quoting correlated markets
- Reduced incoherence and exploitable contradictions across correlated markets
- A scalable framework: N events → 2^N worlds (manageable with small N and can be extended with structured factor models later)
This design turns multiple correlated prediction markets into a single multi-dimensional joint-outcome market, where:
- the world table is the source of truth
- marginals/slices/corners are just different views/contracts
- an LMSR AMM updates all prices coherently
- liquidity is pooled instead of fragmented
The result is a market that is simpler for users, more capital-efficient for liquidity providers, and more consistent overall.
This project integrates two hackathon partner tracks to create a complete prediction market experience.
What It Does: Yellow Network's Nitrolite SDK enables high-frequency betting through App Sessions (state channels), allowing instant, gasless transactions during a trading session.
How We Use It:
We implemented a complete App Session lifecycle using Yellow Network's Nitrolite SDK (@erc7824/nitrolite):
| Phase | Implementation |
|---|---|
| Authentication | EIP-712 wallet signature + ephemeral session keys for gasless operations |
| Session Creation | User & CLOB server co-sign CreateAppSession message to establish P2P state channel |
| Off-Chain Trading | Each bet updates channel balance via RPCAppStateIntent.Operate — no gas, instant confirmation |
| Balance Management | Support for Deposit and Withdraw operations during active session |
| Session Close | Liquidate positions, withdraw funds, and close channel with final settlement on Yellow Network |
Key Components:
- Custom React Hook (hooks/useYellowSession.ts) — 895-line implementation managing WebSocket RPC, co-signing, and session lifecycle
- CLOB Server Integration (server.js) — Backend co-signs all state updates and maintains session state
- Co-Signing Architecture — Both user and CLOB must approve every state transition (Nitro protocol requirement)
Why Yellow Network:
- Gas efficiency: 100+ bets = 1 on-chain tx (only at session close)
- Instant UX: Bets feel like Web2 (no waiting for block confirmation)
- Session logic: Natural fit for "betting sessions" (deposit → trade many times → withdraw)
- WebSocket RPC: Real-time state synchronization via Yellow Network clearnode
What It Does: ENS provides human-readable identity for users and enables direct token purchases via Metamask through market subdomains.
How We Use It:
| Feature | Implementation |
|---|---|
| Username Display | Use wagmi's useEnsName() to show vitalik.eth instead of 0xd8dA... throughout the UI |
| Avatar Resolution | Fetch ENS avatar records via useEnsAvatar() for profile pictures |
| Profile Component | EnsProfile.tsx — Displays ENS name + avatar in navbar |
| Trade Attribution | Associate all betting activity with ENS names for reputation/leaderboards |
We register 8 ENS subdomains per market (one for each corner outcome) using the ENS NameWrapper on Sepolia:
Parent Domain: onlytruth.eth
Subdomain Pattern: {marketName}-{corner}.onlytruth.eth
Example for "Iran War 2026" market:
| Corner | ENS Subdomain | Resolves To |
|---|---|---|
| 000 (No/No/No) | iranwar-nnn.onlytruth.eth |
CornerReceiver[000] contract |
| 001 (No/No/Yes) | iranwar-nny.onlytruth.eth |
CornerReceiver[001] contract |
| ... | ... | ... |
| 111 (Yes/Yes/Yes) | iranwar-yyy.onlytruth.eth |
CornerReceiver[111] contract |
How It Works:
-
Subdomain Creation (useMarketSubdomains.ts):
- Uses ENS NameWrapper's
setSubnodeRecord()to create subdomain - Uses Public Resolver's
setAddr()to point subdomain to CornerReceiver contract
- Uses ENS NameWrapper's
-
Wallet-Native Purchase Flow:
- User sends ETH to
iranwar-yyy.onlytruth.ethfrom Metamask - ENS resolves to CornerReceiver contract address
- CornerReceiver forwards to SwapRouter which mints outcome tokens
- SwapRouter emits
CornerPurchasedevent
- User sends ETH to
-
Event Listening (ensListener.js):
- CLOB server polls for
CornerPurchasedevents - Automatically credits buyer's session and executes market buy
- CLOB server polls for
ENS Contracts Used:
- NameWrapper (
0x0635513f179D50A207757E05759CbD106d7dFcE8) — Subdomain creation - Public Resolver (
0xE99638b40E4Fff0129D56f03b55b6bbC4BBE49b5) — Address record management
Why ENS:
- Trust: Users see
vitalik.ethnot0x1234...— builds recognizable identity - Discoverability: Markets have memorable names, not contract addresses
- Wallet-Native UX: Buy tokens by sending ETH to ENS name (no dApp required)
- Composability: Any wallet/dApp can resolve market contracts via ENS
Step 1: User "vitalik.eth" opens betting session
- ENS resolves
vitalik.eth→ wallet address viauseEnsName() - Yellow SDK creates App Session (state channel) between user and CLOB
- User deposits 100 ytest.usd into channel via
createAppSession()
Step 2: User places bets (gasless via Yellow)
- User selects corner "111" (Yes/Yes/Yes) and bets $5
sendPaymentToCLOB(5)transfers funds off-chain viaRPCAppStateIntent.Operate- CLOB server co-signs state update
- Trade executed on order book against vAMM or user limit orders
- No gas fees, instant confirmation
- UI shows "vitalik.eth betting..." with ENS avatar
Step 3: User ends session
closeSession()liquidates all positions to USD- Final withdrawal via
RPCAppStateIntent.Withdraw CloseAppSessionmessage settles on Yellow Network- User's ledger balance updated (one on-chain settlement)
Step 4: Market Resolution
- Oracle resolves events A, B, C
- Winning corner identified (e.g., corner 111)
vitalik.ethredeems winning tokens for final payout
Alternative: ENS Direct Purchase
- User sends ETH to
iranwar-yyy.onlytruth.ethfrom Metamask - ENS resolves to CornerReceiver contract
- CLOB server detects
CornerPurchasedevent and executes market buy - Outcome tokens delivered to buyer's address
| Track | Role | Benefit |
|---|---|---|
| Yellow Network | Payment layer (App Sessions/State Channels) | Gasless betting, instant UX, session-based trading |
| ENS | Identity + Market Discovery | User profiles, wallet-native token purchases via subdomains |
Together with our custom vAMM (LS-LMSR), they create a prediction market that:
- ✅ Trades like Web2 (instant, gasless via Yellow App Sessions)
- ✅ Provides coherent pricing (vAMM updates all 8 corners atomically)
- ✅ Feels human (ENS names for users and markets)
- ✅ Works from any wallet (send ETH to
iranwar-yyy.onlytruth.ethto buy)
We combine markets when they share most of:
- Same domain/driver (same geopolitical conflict, same company, same macro theme)
- Similar time window (or clearly modelable time structure)
- Non-contradicting resolution sources (same oracle / same definitions)
- Expected correlation is strong enough that shared liquidity helps more than it confuses
For each market question, we extract a structured "context card":
| Field | Examples |
|---|---|
| Entities | United States, China, Donald Trump |
| Event type | strike / resignation / sanction / election |
| Region | Middle East, Europe |
| Time window | by [date] |
| Causal theme | escalation / regime change / conflict |
We then compute similarity and group:
Step A: Similarity Score
- Text embedding similarity (semantic)
- Overlap in entities
- Overlap in event type
- Overlap in time window
Step B: Cluster
- If similarity > threshold → same "cluster"
- Cap cluster size for MVP (3–5 markets)
Step C: Human/Rules Guardrails
- Don't combine if time windows differ too much
- Don't combine if resolution criteria differ ("strike" definitions)
This produces market clusters (e.g., "combine these 3 markets") without heavy statistical modeling.
We derive the world table by assuming a latent "driver" variable (e.g., E = escalation level) that captures the correlation structure.
E = 0 → calm
E = 1 → high escalation
P(E=1) = 0.30
P(A=Yes | E=1) = 0.70, P(A=Yes | E=0) = 0.10
P(B=Yes | E=1) = 0.60, P(B=Yes | E=0) = 0.05
P(C=Yes | E=1) = 0.80, P(C=Yes | E=0) = 0.10
P(A,B,C) = Σₑ P(E=e) · P(A|e) · P(B|e) · P(C|e)
This automatically creates a coherent 8-world table that:
- Makes A/B/C positively correlated via E
- Provides a reasonable starting "shape"
- Market trades then override and reshape this prior
- Context clustering — markets in the same escalation cluster share the same latent driver
- Historical learning — optionally refined over time from market data
This is a compelling story: we start with a structured prior, then traders move it.
We use a single AMM over corners only.
The AMM lives on the 8 corners (000…111). Marginals and slices are not separate markets — they are basket trades of corners.
| Contract Type | Price Formula |
|---|---|
| Slice (A=1, B=1 regardless of C) | p₁₁₀ + p₁₁₁ |
| Marginal (C=Yes) | p₀₀₁ + p₀₁₁ + p₁₀₁ + p₁₁₁ |
| Corner (exact world) | pᵢⱼₖ directly |
Users can "trade slices/marginals" in the UI, but the backend executes as a basket of corner trades at AMM prices.
Benefits:
- ✅ No price mismatch possible
- ✅ No need for merge/split arbitrage logic
- ✅ Much simpler implementation
If slices traded directly as separate tokens, merge/split conversion would be required:
- Merge:
110 + 111 → slice(AB) - Split:
slice(AB) → 110 + 111
If the slice token got overpriced vs corners, arbitrage would sell slice and buy corners. We avoid this complexity by using corners-only AMM + basket UI.
We represent every position internally as exposure over the 8 corners. Cancellation happens automatically by normal addition/subtraction.
We store a portfolio vector x[000..111] = how many $1-per-share claims owned on each corner.
| Action | Effect |
|---|---|
| Buy corner 110 | x[110] += 1 |
| Buy slice AB (110+111) | x[110] += 1, x[111] += 1 |
| Sell corner 111 | x[111] -= 1 |
User buys slice AB = (110 + 111)
→ x[110] = 1, x[111] = 1
User sells corner 111
→ x[111] -= 1
Net result:
→ x[110] = 1, x[111] = 0
The "regardless of C" exposure is gone — now it's a pure corner bet on (1,1,0).
Positions are always stored in corner-space, so netting is exact — no manual cancellation needed.
After netting, we compress the display for users:
- If
x[110]andx[111]are both 5 → show "5 shares of slice(AB)" - If only one exists → show corner
This feels magical for users, but it's just algebra on the corner vector.
Updating 8 markets per trade is trivial for the machine.
- We run one AMM that outputs 8 corner prices
- Marginals/slices are derived (sums of corner prices)
- The UI shows a small set; all 8 corners are only shown in "advanced mode"
Why We Use LMSR:
- Naturally supports many outcomes
- Always produces a coherent probability distribution (sums to 1)
- Single update formula handles any basket trade
User trades marginal A=Yes
→ AMM updates corners {100, 101, 110, 111}
→ All 8 prices recalculate via softmax
→ All derived marginals/slices update automatically
The computational cost is O(2^N) per trade, which is trivial for N ≤ 5 (32 outcomes).
We utilize the Yellow Network Nitrolite SDK to establish App Sessions—high-performance, direct off-chain channels between the User (Frontend) and the Market Maker (CLOB Server).
-
useYellowSessionHook:- Manages the full lifecycle:
Create,Deposit,Trade(Operate),Withdraw,Close. - Handles Session Key generation and EIP-712 authentication.
- Maintains the WebSocket connection to the Yellow Network.
- Manages the full lifecycle:
-
Market Client (
marketClient.ts):- Interfacs with the CLOB server to fetch market data (Order Book, Prices).
- Exchanges Partial Signatures with the counterparty (CLOB) to co-sign state updates.
-
Session Creation:
- User signs
CreateAppSessionintent. - CLOB co-signs.
- Double-signed message submitted to Yellow Network.
- Result: Instant P2P channel established.
- User signs
-
Trading (Off-Chain):
- User signs
RPCAppStateIntent.Operate(Update Balance). - CLOB validates trade & co-signs.
- State updated instantly. No Gas. No Blocking.
- User signs
-
Settlement:
- On session close, final balances are settled on the Yellow Network overlay.
- Funds can be withdrawn to L1/L2.
For a deep dive into the code implementation, see Yellow Implementation Docs.
