The bet you can be partly right about.
Every market is one integer sports stat against a threshold. Ladders, props, spreads and totals are baskets of those.
Every market asks one question
Did the stat land at or above the threshold? That is the whole contract. value >= threshold, on an integer, for every sport.
Ohtani finishes with 2 hits. The >=1 market pays long. The >=2 market pays long. The >=3 market pays short. One integer settled all three, and it settled every ladder built on top of them at the same instant.
There is no under
The complement of >=2 is <=1. So the SHORT token is the under, and no market in the system carries a direction field. Someone buying "under 2 hits" is buying SHORT on the >=2 market, in the same order book as everyone buying the over.
The interface says "under 2 hits". The chain says SHORT. If a separate under market ever appears, the book has re-fragmented and the gain is gone.
The tiered ladder
A single over/under is a cliff. You stake $60 on over 1.5 hits. A one-hit night pays exactly what a hitless night pays. Nothing. That is not how anyone watches a game. "He is going to have a big night" is a statement about degree, and a binary throws the degree away.
The ladder prices the degree. Buy three rungs at once, $20 on each:
| Rung | Price | Multiplier | Tokens | Pays |
|---|---|---|---|---|
| >= 1 hit | 0.833 | 1.20x | 24 | $24 |
| >= 2 hits | 0.556 | 1.80x | 36 | $36 |
| >= 3 hits | 0.417 | 2.40x | 48 | $48 |
The rungs are nested, so they settle together and they settle in order. One hit pays the first rung. Two hits pay the first two. Three pay all of them. The payout climbs with the performance instead of flipping once.
What it costs you
The ladder is not a better bet. It is a different shape, and the shape is worth stating plainly:
| Final stat | Ladder | $60 on over 1.5 | |
|---|---|---|---|
| 0 hits | $0 | $0 | tie |
| 1 hit | $24 | $0 | ladder |
| 2 hits | $60 | $107.91 | binary |
| 3 hits | $108 | $107.91 | tie |
The ladder gives up the middle to buy the bottom. A bettor who would have collected $107.91 at exactly two hits collects $60 instead, and that is the price of collecting $24 on a night the binary pays zero. Sell the smoothing, not a free lunch.
Why the contract stays boring
A ladder is not an object in any contract. It is three ordinary threshold binaries bought in one transaction, which is what keeps it liquid.
>=1/>=2/>=3 and >=1/>=2 would be two different non-interchangeable things, and liquidity would shatter across the entire product space. Market makers shape the whole payout curve by quoting prices, and never touch a tier object at all.
So new tiers ship without touching a contract. A five-step ladder for a marquee player and a two-step one for a bench bat are both just orders, and both draw on the same pooled depth as every other ladder on that stat.
Four hashes, no registry
Identity is derived, never assigned. There is no factory and no registry of registries.
// "Ohtani, hits, game 12345" statKey = keccak256(eventId, subjectId, statId) // "...is >= 2" marketId = keccak256(statKey, threshold) longId = uint256(keccak256(marketId, LONG)) shortId = uint256(keccak256(marketId, SHORT))
Creating a market is permissionless and idempotent. Two people who ask for the same market get the same marketId, because the id is the description.
Thresholds must sit on the stat's tick. Hits tick by 1, rushing yards by 25, passing yards by 50, and createMarket rejects anything off-tick. Football needs yard tranches while baseball needs single integers, and putting granularity on the stat lets one contract serve both.
Multi-game windows
A market on a player's total across three games needs zero contract changes, because eventId is opaque to the contract. A window is a different value in the same hash:
eventId = keccak256( "xo/window/1|mahomes|passing_yards_3game|401547,401561,401578|by=1700604800" )
The hash binds the game set to the market whether or not anyone publishes it. XOWindows publishes the preimage, so the answer to "which three games was this?" is not "trust our JSON file". Registration is permissionless and cannot lie. register derives the eventId from its arguments rather than accepting one, so the only thing anyone can publish is a preimage that hashes to the market it names.
XOMarkets means the code on the money path does not grow to carry a registry.
One invariant holds the money
Deposit a dollar and split returns one of each. Hand both back and merge returns the dollar, as long as the market is still live. Once the stat resolves the winner redeems for a dollar and the loser is worth nothing.
merge closes at resolution. Every LONG and SHORT pair is worth exactly one unit right up to the moment one of them is worth zero, so leaving merge open past that would hand anyone holding both sides a free exit around the payout fee.
Redemption does not depend on us
redeem burns a winning position and pays the holder. redeemFor takes a list of holders and sweeps their full balances, and the payout always goes to the holder rather than the caller.
Transfers stay open after resolution too. A resolved winning token is a claim on a dollar, and there is no reason the contract should stop someone selling it.
The exchange custodies nothing
Makers sign EIP-712 orders off-chain. Takers fill them on-chain. There is no operator role, no sequencer and no privileged matcher. A taker's transaction is their intent, and anyone holding a signed maker order can fill it. Between transactions the exchange holds nothing at all.
Price is makerAmount / takerAmount. The multiplier retail sees is its reciprocal, which is how a market maker draws the whole ladder curve with three ordinary limit prices.
Three ways a fill can settle
Taker buys LONG, maker buys SHORT. Pull 0.83 from the taker and 0.17 from the maker, mint one fresh pair, send LONG one way and SHORT the other.
Taker buys, maker sells the same token. A straight swap of tokens that already exist.
Taker sells LONG, maker sells SHORT. Burn the pair and release the collateral. This is the only exit that removes a position from the system.
The taker names the path explicitly and the contract never infers it from balances. Auto-detection would mean a user who happens to hold SHORT silently gets a different trade from one who does not, and an explicit path guarantees no fill ever pulls capital the taker did not ask to spend.
A ladder is one transaction or none
fillOrders is strict. Every named order fills its exact signed amount, or the whole batch reverts. There is no partial matching and no slippage parameter, because with strict atomicity the total cost is already fixed at signing time and nothing can drift.
Fills die at resolution
The fill path refuses a resolved stat. This is the most important check in the system, because there are no open orders on-chain to cancel. An order is a signature sitting in a backend and in a maker's client, so resolution cannot reach out and close it.
Otherwise a maker quotes SHORT(>=2) at 0.17, goes to dinner, and the stat comes in at 1. Anyone watching takes a guaranteed 0.83 off them. Heater deliberately runs no trading cutoff at the end of a game, so the resolution boundary is the only fence in the system and it has to be real.
Repricing on a fast chain
The target L2 produces roughly ten blocks a second, so a maker repricing a ladder is a routine action rather than an emergency. There are four ways to pull a quote, and they differ in how much they take down:
| Call | Retires | Cost |
|---|---|---|
| cancelOrder | one order | per order |
| cancelOrders | a named batch | per order, one tx |
| cancelScope | one group, any size | 34.6k gas flat |
| cancelAll | every quote the maker has | flat |
A maker groups a ladder under a nonceKey of its own, conventionally the statKey, which is exactly the set of rungs that move together. Retiring a fifty-rung ladder then costs the same as retiring a three-rung one, and every other market the maker quotes keeps standing. cancelAll is still the panic button.
Resolution, described accurately
A Heater-operated attestor reads public sports data over zkTLS, appends the reading to a hash-chained public log, and signs the log entry. The signature carries a sourceHash that commits to that entry, and because the entry commits to every entry before it, each on-chain resolution anchors the whole log to a block.
Submitting it is permissionless. The signature is the gate, so anyone holding a valid attestation can relay it and Heater cannot stall a resolution by declining to send a transaction.
Call it a verifiable public log, not trustless
zkTLS is designated-verifier. A smart contract cannot take part in a TLS session, so it can never be the verifier, and on-chain there is always an attestor signature standing in for a proof. The entire forgery guarantee rests on the prover and the notary being different parties. Heater currently runs both.
The controls sit before the signature
There is no settlement delay and no on-chain correction path, because a delay only helps if you can act during it. Resolution is immutable, a second resolve reverts, and there is no void, so a delay would just mean watching the money leave on schedule. Every control therefore runs in the attestor, before it signs:
- Two independent sources must agree. Different endpoints with different shapes, not the same URL fetched twice. A renamed field, a broken path or a stale cache dies here.
- The read waits out the correction buffer. Official scorers revise for hours after a game, and a three-game window sits through one such period per game.
- The value sits inside a sanity band against the last observation, and inside the stat's registered range on-chain. 150 after game two and 4,700 in the aggregate should page a human, not settle.
- A dry run prints which rungs flip. An operator reviewing "the >=650 rung flips from losing to winning and $X moves" catches what reviewing the number 680 does not.
- Every failure reports at once, because an operator at 1am should see every problem rather than fix them one at a time.
There is no void
Every stat resolves to an integer, and a game nobody played contributes zero. That removes a discretionary lever from the system rather than adding a safety valve to it.
Two fees
A trading fee on fills, proportional to min(price, 1 - price) rather than flat basis points on notional. A flat fee taxes a 0.10 leg and a 0.83 leg the same in absolute terms, even though the 0.10 holder is risking a tenth as much. That bends the ladder curve at exactly the tiers that make the product worth building. Makers cannot set it above 10%.
A payout fee on redemption, in basis points, capped at 10% at deploy. This is the sportsbook model, a rake on winnings, and it is largely invisible to market makers who net out.
(1 - payoutFee) x P, so makers quote slightly wider. The payout fee is also only enforceable because merge closes at resolution.
Where this actually is
Four contracts, around 770 lines of Solidity. A local stack brings up a chain, the contracts, the attestor and the order book, and a scripted run drives both sides of a market end to end. Golden vectors pin the Solidity and the TypeScript to each other, so an EIP-712 digest or an ABI that drifts between the two fails a test rather than reverting a fill.
Testnet, with test tokens, and freely redeployable. No proxies, no upgradeability, no governance and no admin escape hatches, because iterating by redeploying is faster and leaves far less to attack.
Deliberately not built
- Nested-threshold netting. Long
>=2and short>=3always pays at least 1, so the contract could release that collateral. Real capital efficiency, real complexity on the money path, and not what a young book needs first. - An AMM. Evaluated and deferred. An LMSR would make ladder monotonicity structural, so a ladder could never invert and price
>=3above>=2. The order book has to prove demand first. - Names on-chain. Markets are hashes. The read API returns ids and the interface joins them to human labels off-chain.