How Presage works
Presage is a set of smart contracts for parimutuel prediction markets and 1v1 duels on tokenized stocks, live on Robinhood Chain. Every market settles itself from a 30-minute on-chain price average — no oracle feed, no judges, no disputes. This is the technical reference.
A market that settles itself
Traditional prediction markets need an oracle or a human to declare the winner. Presage removes that trust assumption: each stock already trades against USDG in a Uniswap v3 pool on Robinhood Chain, so the market reads the stock's own on-chain price to decide the outcome. Nobody adjudicates — the tape does.
There are two ways to play, sharing one settlement engine:
- Prediction markets — a shared parimutuel pool where everyone bets YES or NO on where a stock goes.
- Showdown duels — a private 1v1 wager: your champion stock against a rival's, highest return wins.
Everything below is enforced by the contracts — not the front end. The app is just a convenient window onto state anyone can read or write directly.
Five contracts, one vault
Funds live in exactly one place. HoodBet is the parimutuel core: it escrows every stake, tracks who bet which side, pays the fee, and settles claims. It never decides outcomes itself — it asks a resolver. TwapResolver supplies the automated price templates; AdminResolver is a manual fallback for edge cases. StockDuel runs the 1v1 Showdown game with its own escrow, and ProfileRegistry holds optional on-chain names.
Traders touch the resolvers; only HoodBet holds funds. Prices come from the same pools you can swap on.
Parimutuel pools
There is no house taking the other side and no fixed odds. When you call bet(id, side, amount), your USDG is added to that side's pool. The implied probability of YES is simply its share of the total: poolYES / (poolYES + poolNO). As money arrives, the odds move.
At resolution the losing pool is handed to the winning side, pro-rata to each winner's stake, after a 2% fee. Winners also get their own stake back. If a side is empty, there is nothing to contest and everyone is refunded in full.
prize = poolLOSING × (1 − 0.02)
payout = s + prize × ( s / poolWINNING )
Parimutuel: the losing side funds the winners. Empty side ⇒ everyone is refunded in full.
Three market templates
Anyone can create a market permissionlessly from one of three shapes. All comparisons happen in Uniswap tick space, where one tick is 0.01% of price — so a strike is just a tick, and returns are already logarithmic.
Up / down
The simplest call. At creation the resolver records the stock's current TWAP tick as the reference. YES (up) wins if the settlement TWAP is strictly above that reference.
Over / under
You name a price; the app converts it to a strike tick. YES wins if the stock finishes strictly above the strike. Same math as up/down, but the reference is your chosen level instead of the launch price.
Stock duel
Two stocks, one window. The resolver snapshots both TWAP ticks at creation and compares their tick deltas (log-returns) at settlement. Side A wins on a strictly larger move; a tie resolves to B, as stated in the market rules.
30-minute TWAP resolution
Every price the protocol trusts is a time-weighted average price over the last 30 minutes, read straight from the Uniswap v3 pool's observation history via observe([1800, 0]). The pool stores a running tick cumulative; the average tick over the window is the difference of two samples divided by the elapsed seconds (rounded down, matching Uniswap).
Resolution reads a time-weighted average, never the last trade — a one-block spike can't move it.
Because it is an average, not a spot read, an attacker can't swing the outcome by pushing the price for a single block right before resolution — they'd have to hold the price off-market for the whole window, which costs far more than any pot. Tick space is then converted to a human price with price = 1.0001^tick, corrected for whether the stock is token0 or token1 in the pool.
Market lifecycle
A market is created with a closeTime (betting deadline) and a resolveTime. The contract enforces resolveTime ≥ closeTime + 1 hour — the lock gap. Once resolveTime passes, anyone can call resolve(); winners are motivated to do it promptly, and the app auto-resolves too. Then each winner calls claim() to withdraw.
Betting always closes at least an hour before the TWAP is sampled, so no one bets on a half-settled outcome.
Why it can't be gamed
- TWAP, not spot — the 30-minute window makes last-second price pushes worthless.
- Lock gap — betting ends an hour before the price is sampled, so you can never bet on a half-known result.
- Cardinality floor — a pool must carry at least 100 observations of history to be tradable; thin pools, whose averages are cheap to move, are rejected outright.
- Real-pool check — a market only lists if the pool is genuinely paired with USDG and holds live liquidity, filtering out dead or out-of-range pools with bogus prices.
- No privileged oracle — resolution is a pure function of public pool state; there is no feed to bribe or halt.
The owner key can only cancel a market (triggering full refunds) and collect the 2% fee. It cannot pick a winner or touch a winner's stake.
Showdown — 1v1 duels
A Showdown is a direct wager. You call create(pool, stake, duration) with your champion stock and the amount you're risking. A rival answers with accept(id, poolB), matching your stake with their own stock. At that instant both TWAPs are snapshotted and the clock starts; duration runs from 1 hour to 30 days.
When the window closes, the contract compares each champion's log-return from its snapshot. The larger move wins the whole pot minus a 2% fee; a genuine tie refunds both sides. Until someone accepts, the creator can cancel and reclaim their stake.
Both champions are measured from the same starting instant, so the duel is a pure relative-return race.
Daily rounds
Alongside user-created markets, a scheduled job publishes fresh up/down rounds each session on deep-pool names and settles them at U.S. market close. They're ordinary HoodBet markets — same pools, same TWAP, same claim flow — just created on a cadence so there is always something live to trade.
Fees & payout
The fee is a flat 2% (FEE_BPS = 200), and it only ever applies when there is a real contest with a winner and a loser:
- Markets (HoodBet) — 2% of the losing pool. Winners split the remaining 98% pro-rata and keep their own stake in full.
- Duels (StockDuel) — 2% of the whole pot; the winner takes the other 98%. A draw refunds both stakes with no fee.
- Refund cases — canceled markets, empty-side markets, and tied duels return every stake untouched.
On-chain identity
Presage needs no accounts. Your wallet address is your identity: a small rolling hash of the address deterministically derives both a readable pseudonym and an avatar seed, so the same address always shows the same name and face — on any device, with no database behind it.
Same address ⇒ same face and name, on every device, with zero server state.
Two optional layers sit on top: a locally stored display name and photo you control in your browser, and an on-chain name claimed through ProfileRegistry for anyone who wants a portable, verifiable handle.
Reference
Deployed contracts — Robinhood Chain
| Contract | Role | Address |
|---|---|---|
| HoodBet | Parimutuel escrow + accounting | 0xfcfbb9365ffb00d153ba73162c178916855f8b3b |
| TwapResolver | Automated price templates | 0x9249c9EeDdd5188eaC6E86c804a1255249F1CEc3 |
| StockDuel | 1v1 Showdown duels | 0x646c1bf51d20ce0d794fbaebdb9bc17a110c9d9f |
| AdminResolver | Manual resolution fallback | 0x6fc7dbbd27039d77f4f22d10e67938e4bddb4d31 |
| ProfileRegistry | Optional on-chain names | 0x47c9908766ef11f69caf120134f6e9c6ab23201b |
Protocol constants
| Constant | Value | Meaning |
|---|---|---|
| TWAP_WINDOW | 30 minutes | Averaging window for every price read |
| LOCK_GAP | 1 hour | Minimum gap between betting close and resolution |
| FEE_BPS | 200 (2%) | House fee — losing pot (markets) / whole pot (duels) |
| MIN_CARDINALITY | 100 | Minimum pool observation history to be tradable |
| MIN_DURATION / MAX_DURATION | 1 hour / 30 days | Showdown duel length bounds |