market design live web3 dapp

On-Ledger Market for Event Contingent Claims

A deliberately simple on-chain market, intended to demonstrate key infrastructure and functionality: on-chain transactions, on-chain collateral, account abstraction, on-chain role management, wallet-native trading, and contract management

Practice mix
Legal Build Model Spec Research Teach

Context

A thin on-chain market venue

Intent, mechanism sketch, and where the dapp runs — infrastructure first, composable claim tokens later.

Intent

This is an intentionally thin version of a market venue. The goal is to demonstrate the web3-adjacent surface: wallet connect, network selection, faucet and approval flows, transaction funding, and the operator roles that make a live market operable.

In this version we are satisfied when the dapp connects to an on-chain network and transactions are handled through the wallet — without yet optimizing for composability, computational efficiency, cost, or speed. The intent is to build the infrastructure guardrails first and refine design features later.

Claim representation stays simple on purpose, so the main benefit of on-chain markets — composability through standardized asset representation — is not leveraged yet. Market positions live as balances inside the market contract’s ledger, not as transferable tokens. That reduces the complexity of token design and asset handling, but it also undercuts a core reason to go on-chain: balances sit on a contract-native ledger instead of as token positions custodied by the user’s wallet.

What it is

For this demonstration we could have used any market type; we chose a market scoring rule (MSR) prediction market. We trade event-contingent claims in the form of Arrow–Debreu securities, with the following payout structure:

XYes={1if the event occurs0otherwiseX_{\mathrm{Yes}} = \begin{cases} 1 & \text{if the event occurs} \\ 0 & \text{otherwise} \end{cases}
XNo={0if the event occurs1otherwiseX_{\mathrm{No}} = \begin{cases} 0 & \text{if the event occurs} \\ 1 & \text{otherwise} \end{cases}

The market is intentionally simple: users can buy these claims at the current marginal market price (implied by inventory state and the cost function), then hold them until resolution or sell them back through the same mechanism. That design yields an always-liquid primary and secondary market.

Where it runs

The dapp is live at predictionmarket.tokenengineeringlabs.com, deployed on both Ethereum Sepolia and the UZH ETH PoS chain. Users pick the network in-app and match it in the wallet.

Prediction Market header with network dropdown open: Sepolia selected, UZH ETH PoS available, wallet connected with PTE balance.
Network selection in the header — switch between Sepolia and UZH ETH PoS; the wallet must be on the same chain. Claim / approve status and PTE balance sit alongside the connected address.

Product surface

Operating the market

Gas and PTE, wallet onboarding, and the role / lifecycle controls that make a live market operable.

This design works with only two tokens

To operate the market the user needs only two distinct assets on the chosen network:

  • L1 gas (native ETH) — on Sepolia, native ETH from a public faucet; on UZH ETH PoS, native ETH obtained through UZH. Gas is required because every interaction with the ledger or contract is an on-chain transaction: each trade, role update, approval, market creation, or resolution. Designated functions run (for example to price a trade or update a role) and results are stored in the contract — but each of those steps is still an on-chain transaction.
  • PTE (“Prediction Token”) — a separately deployed ERC-20 on each chain. It collateralizes the market contracts and acts as a quasi-stablecoin / unit of account. That gives the sandbox a stable, transferable asset without relying on an external universal collateral, avoids unnecessary financialization, and lets issuance and supply rules be defined independently. By default each wallet may claim once from the PTE faucet; further balances come from active market participation. The PTE contract also exposes a mint function the deployer can call if needed.

The two tokens have distinct roles: gas moves the state machine; PTE prices beliefs — buy and sell exposure, lock collateral, and settle winnings when a market resolves.

Wallet onboarding

For demonstration, each new wallet goes through a short onboarding flow before it can trade: connect the wallet, match the app network, claim PTE once from the faucet, approve the market contract to spend PTE, then open an active market.

Get started onboarding checklist: connect wallet, use Sepolia network, claim tokens, approve tokens, then trade on a market.
Onboarding checklist — connect → network → claim PTE → approve → trade. Steps unlock in order so a fresh wallet ends up funded and permissioned before the first buy.

Roles and control surface

The system separates who may change privileges, who may operate markets, and who may trade. On-chain roles gate the privileged actions; anyone with PTE and an approval can participate as a trader.

  • Contract deployer / admin — mint and transfer PTE, grant or revoke rights, and other privileged token / system controls.
  • Market manager — create and close markets, resolve or invalidate them, and withdraw surplus after settlement.
  • Market participant — buy shares at the current LMSR price, or sell them back for collateral at the then-implied prices. In both cases the trader moves the market inventory (qA,qB)(q_A,q_B); the cost function decides how much PTE must be deposited or refunded for that move. Details are in the cost-function section below.

Grant and revoke rights

Admins manage a role matrix per address: market admin, create / close / resolve / withdraw, token admin, and mint. Rights can be granted or revoked per cell on-chain.

Role matrix table for an address with Adm, Create, Close, Resolve, Withdraw, and Mint permissions, including Revoke actions.
Role matrix — current on-chain roles for an address; grant or revoke per capability (admin transfers stay guarded to avoid accidental lockout).

Create a market

A market manager opens a binary question, sets trading duration, and chooses liquidity b[10,1000]b\in[10,\,1000]. Creating the market pulls the LMSR seed C(0,0)=bln2C(0,0)=b\ln 2 from the manager’s wallet.

Create market form: question, Yes/No options, trading duration, and liquidity parameter b with LMSR seed subsidy.
Create market — question, options, duration, and b. Example seed at b = 100: ≈ 69.31 PTE.

Close, resolve, and settle

After trading, the manager walks a fixed lifecycle: end trading, pick the winning outcome, let winners claim (or batch-settle), then withdraw any surplus once collateral covers outstanding winner liability.

Four-step market lifecycle: close trading, resolve winner, winners claim, withdraw surplus.
Market lifecycle — close trading → resolve winner → winners claim → withdraw surplus.

Trade

Participants see live LMSR prices for Yes / No and buy the side they believe. The listed price is the marginal C/q\partial C/\partial q; the amount paid on a finite trade is still the path cost along C.

Active market card asking Will the demo work? with Yes/No buttons at 50/50 LMSR prices.
Active market — Yes / No at the current LMSR prices; a click starts the buy flow against the on-chain inventory.

Mechanism

Economic design

How the market works as a mechanism: what is being traded, how prices form, and what liquidity parameter b buys you. This is the theory behind the screens above.

Arrow–Debreu claims

Each Yes / No share is an Arrow–Debreu security on the market’s event: a contingent claim that pays one unit of PTE in exactly one state of the world, and nothing otherwise.

XYes={1if the event occurs0otherwiseX_{\mathrm{Yes}} = \begin{cases} 1 & \text{if the event occurs} \\ 0 & \text{otherwise} \end{cases}
XNo={0if the event occurs1otherwiseX_{\mathrm{No}} = \begin{cases} 0 & \text{if the event occurs} \\ 1 & \text{otherwise} \end{cases}

Holding one Yes and one No therefore spans the full state space: exactly one of the two pays 11 PTE at resolution. LMSR does not change that payoff structure — it prices how much PTE it costs to move inventory of these claims before the event resolves.

Ledger claims, not tokens

When you buy shares, the contract updates an internal mapping: your address holds more A or B against that market id. There is no ERC-20 (or ERC-1155) share token to transfer, collateralize, or plug into another protocol.

That is the central design tradeoff. The ledger is enough to trade, resolve, and settle. It is not enough for composability — the positions cannot travel through the broader DeFi ecosystem. Later projects in this series tokenize claims precisely to lift that constraint.

Market logic: LMSR

Prices come from Robin Hanson’s logarithmic market scoring rule (LMSR) — a cost-function automated market maker. There is no order book. The contract always quotes from outstanding share inventories, so markets remain tradeable even when interest is thin.

State

A binary market tracks outstanding shares qA, qB ≥ 0 and a liquidity parameter b > 0 chosen at market creation. In this deployment the creator may set any b in b[10,1000]b\in[10,\,1000] (in PTE units).

Cost function

The market maker’s cost function is

C(qA,qB)=bln ⁣(eqA/b+eqB/b)C(q_A, q_B) = b \ln\!\bigl(e^{q_A/b} + e^{q_B/b}\bigr)

C is the total amount traders have collectively paid into the AMM for the current inventory. It is strictly convex, symmetric in the two outcomes, and increasing in each share count.

Trading: buy and sell

To buy Δ shares of outcome A, the trader pays the path cost along the cost surface:

cost=C(qA+Δ,qB)C(qA,qB)\mathrm{cost} = C(q_A+\Delta,\, q_B) - C(q_A,\, q_B)

Selling Δ shares of A returns the reverse difference:

refund=C(qA,qB)C(qAΔ,qB)\mathrm{refund} = C(q_A,\, q_B) - C(q_A-\Delta,\, q_B)

The average price paid (or received) is that difference divided by Δ. Because C is convex, buys experience slippage upward from the spot price and sells experience slippage downward — the AMM’s built-in price impact.

3D LMSR inventory space: convex cost surface C over Yes and No share quantities, with the current market state marked on the surface.
Trading on the cost surface: a trade moves inventory (qA,qB)(q_A,q_B) along C; the trader pays C(q)C(q)C(q')-C(q). The green marker is the current state; the slope at that point is the vector of marginal prices.

Prices as probabilities

Instantaneous (marginal) prices are the partial derivatives of C:

pA(qA,qB)=CqA=eqA/beqA/b+eqB/b,pB=1pAp_A(q_A,q_B)=\frac{\partial C}{\partial q_A}=\frac{e^{q_A/b}}{e^{q_A/b}+e^{q_B/b}},\qquad p_B=1-p_A

Always pA, pB ∈ (0, 1) and pA + pB = 1, so they read as implied probabilities. Under standard assumptions LMSR inherits the incentive properties of the logarithmic scoring rule: risk-neutral traders are pushed to move prices toward their true beliefs.

Choosing b: depth, loss, pre-funding

At setup the market creator selects liquidity b[10,1000]b\in[10,\,1000]. That single choice couples three things: how fast prices move, how much the market maker can lose in the worst case, and how much collateral the creator must pre-fund.

Bounded loss

For an N-outcome LMSR the market maker’s worst-case loss is bounded by Lmax=blnNL_{\max}=b\ln N; in the binary case (N = 2) that is

Lmax=bln2L_{\max} = b\ln 2
b = 10Lmax6.93L_{\max}\approx 6.93 PTE
b = 1000Lmax693.15L_{\max}\approx 693.15 PTE

Larger b means deeper markets (smaller price impact per trade) and a proportionally larger worst-case subsidy the creator must be willing to stand behind. The dapp surfaces this as Max Bounded Loss next to live collateral.

Market statistics panel showing holdings, outstanding Yes/No shares, trading collateral, and max bounded loss equal to b ln N.
Market statistics: trading collateral is what traders have paid into the AMM so far; Max Bounded Loss is blnNb\ln N for the market’s chosen b (here b = 100 → 69.31 tokens). That bound scales directly with the create-time liquidity choice.

Pre-funding requirement

At empty inventory, C(0,0)=bln2C(0,0)=b\ln 2 — the same quantity as Lmax. That amount is the natural seed / subsidy the creator pre-funds so every eventual winning share can pay out at face value. Near b = 10, markets are cheap to open but twitchy; near b = 1000 they are smoother to trade but ask for ~100× more PTE locked up front.

Why the market stays tradeable

Order books can freeze with no bid or ask. LMSR never waits for a counterparty — the contract is the maker. Relative to a naïve share-ratio price qA/(qA+qB)q_A/(q_A+q_B), LMSR prices stay closer to 12\tfrac{1}{2} for the same imbalance, which is another way of saying the AMM keeps offering liquidity instead of slamming to the extremes.

Implementation

Technical design

Below we discuss implementation choices and the reasoning behind them:

Dual deployment

The same application talks to contracts on Ethereum Sepolia and UZH ETH PoS — the latter from a collaboration with the University of Zurich, for educational purposes. The system (market contract and token contracts) can be deployed on any chain and wired to the frontend. The only hard requirement is that the network chosen in the header matches the wallet’s chain, so custom networks must be configured in the wallet first. On every chain the user needs enough native gas to execute transactions; PTE can be claimed from the faucet or minted by the deployer. PTE is deployed as a separate ERC-20 per chain, so balances do not bridge automatically.

What is actually on-chain

Market inventory (qA,qB)(q_A,q_B), collateral, share balances, roles, and resolution outcome all live in contract storage. The dapp never “owns” the market state — it only reads over RPC and proposes writes as signed transactions. Share positions are address-to-balance mappings inside the market contract, not separate claim tokens.

Quote vs execution

A trade is not a single number on the UI. True state lives in the on-chain contract and must be queried several times. Trading is a sequence of quotes: the quote for a desired Δ, the maximum acceptable price given that other trades may land first, and a fresh quote shortly before the user commits to execution. Those quotes can move with market conditions. Whether the trade fills or reverts is decided only at execution — the transaction carries a binding max price and the contract recomputes LMSR cost against the inventory that exists then. The UI orients the user; it has no binding say in the outcome.

Shares vs tokens — two questions

The buy flow can quote in either unit. Shares are the number of claims acquired on an outcome; tokens are the PTE collateral offered for the position. Because shares trade at the current market price, the two quantities differ — and they answer different risk questions:

  • Shares (claim size) — “How much can I max win?” You pick how many Arrow–Debreu shares to buy. At resolution a winning share pays 1 PTE, so share count is the maximum payout. The UI then quotes how much PTE that purchase costs right now — the path cost of the inventory move along C.
  • Tokens (PTE budget) — “How much am I willing to bet / lose?” You pick a collateral budget. The UI solves for how many shares that budget buys at the current inventory (still via the cost function), so the downside is framed as PTE spent up front.

Underneath, the same calculation runs; only the control variable changes. A third perspective would also be valuable, but is not implemented yet: marginal prices already imply pooled event probabilities, and any inventory change moves those probabilities. One could ask: given your belief about the event, and current prices pYesp_{\mathrm{Yes}} and pNo=1pYesp_{\mathrm{No}}=1-p_{\mathrm{Yes}}, how much inventory must move to reach that market (price) state? From that desired outcome, both share size and PTE cost would follow.

Step 1 — build a quote

On the trade card the user chooses Shares or Tokens, enters an amount, and (on blur / Enter) the dapp runs an eth_call against the live ledger. That returns an indicative total — e.g. 20 Yes shares → ~10.50 PTE at a 50/50 book. They also set a slippage tolerance (%). For buys this will become a worst-case maxCost vs the confirmation quote; for sells, a minimum refund floor.

Trade quote UI: 20 shares selected, total ~10.50 tokens, 1% slippage tolerance.
Quote step — unit toggle (shares / tokens), size input, simulated total, and slippage tolerance before Continue.

Step 2 — confirm at entry

Continue opens a confirmation that freezes the intent the user typed: side, share count (or budget), cost at entry, and max cost = entry × (1 + slippage). Nothing is on chain yet. The max cost is the bound the wallet will later send with the transaction — not a promise that execution equals the entry quote.

Confirm transaction dialog showing cost at entry and max cost including 1% slippage.
Confirm at entry — intent plus entry quote and slippage-capped max cost, still before a fresh re-quote or wallet submit.

Step 3 — re-quote, then submit

Before the wallet prompt, the UI can refresh cost at confirmation with another eth_call. Entry and confirmation may match (quiet book) or diverge (someone else traded). Submit transaction hands the call to the wallet with the share delta and the max-cost bound derived from the entry quote and slippage.

Confirm transaction with cost at entry, cost at confirmation, max cost with slippage, and Submit transaction.
Pre-submit — entry quote, confirmation re-quote, and max cost; Submit opens the wallet.

What can still go wrong

Even after Submit, the mined result can differ from every on-screen number:

  • Inventory can move again while the wallet is open or while the tx sits in the mempool — other trades change (qA,qB)(q_A,q_B) before this one executes.
  • The contract recomputes C(q)C(q)C(q')-C(q) at execution. If that cost exceeds maxCost (buy) or the refund falls below the floor (sell), the transaction reverts. Slippage tolerance caps downside risk; it does not guarantee a fill — on the contrary, it blocks the fill when the loss would exceed the bound.
  • In the current implementation, allowance or gas failure also reverts the whole call; there is no partial-fill logic.

So the pipeline is: simulate, bound with slippage, then re-check (purely informational, if the market moved in the meantime). Only then sign and execute-or-revert. Binding truth is only the on-chain state at inclusion.

Approvals and gas

Trading PTE requires a standard ERC-20 approve so the market contract may transferFrom collateral. Allowance is a spending cap that applies across upcoming transactions. Gas (native ETH) pays for every state change: connect-side ops, claim, approve, create, trade, resolve, withdraw.

Roles as on-chain gates

Create / close / resolve / withdraw / mint are role checks enforced in the contract. The role matrix is a view onto that AccessControl-style state; grant and revoke are themselves transactions and must be signed accordingly.

Limits

What this version accepts

Deliberate boundaries of the ledger design, and where to read next.

Accepted limits in this version

  • UI quotes are never the settlement price; only the mined LMSR evaluation is — traders wait for that finality. Slippage / maxCost can make a trade revert instead of filling at a worse price
  • Transaction ordering is decided by the block builder: earlier txs see earlier inventory.
  • Positions are not portable ERC-20 / ERC-1155 claims (see economic section above). That is not the end of the design path — ERC-20 / ERC-1155 variants exist to carry the same LMSR economics into tokenized, portable positions, with all the associated implications.
  • All trades execute on the spot: this is neither an order book nor a limit-order venue.

Literature & further reading