How it works.

Written for skeptics. No hand-waving.

Even betting

Traditional sportsbooks build an overround into every line — they price both sides to pay out less than 100 cents on the dollar. The margin is silent and structural.

Even Steven is parimutuel: every dollar staked on the losing side goes to the winning side. No house pool. No overround. Winners split 100% of losers' stakes.

At a liquid market — equal money on both sides — a $100 stake returns $200 gross on a win. 1:1 payout. The house was the hidden cost. We removed it.

The fee, honestly.

There is one fee: 2% of your stake, charged once at bet placement. It never touches the pool.

Stake$100.00
Fee (2%)$2.00
Total cost$102.00
Gross payout at liquidity$200.00
Net profit$98.00

The 2% is set in the contract as FEE_PERCENT = 200 (200 basis points). It is immutable per deployed market — it cannot be changed after the contract is live. There is no settlement fee. No claim fee. No surprises.

What you actually pay.

Every prediction market advertises a low fee. None of them tell you the full cost — because the full cost is the house, and the house is the business.

On an order book you pay three things every trade: the taker fee (the number they advertise), the overround (both sides sum past 100¢ and you pay the gap), and slippage (your order eats the book at progressively worse prices). Rename those three and you have a bookie — the vig, the spread, the line move. They are invisible, variable, and depth-dependent, and they are exactly the margins the house has always taken. Moving them on-chain did not remove them; it hid them in the microstructure.

Even Steven has none of them. No order book means no overround and no slippage. You pay one thing: a flat 2% protocol fee on your stake, charged the moment you place the bet.

scroll →

PlatformTotalDepthFeeOverSlipSettle
Even Steven2.00%Any2.00%0%0%0%
Polymarket US~4.9%Liquid2.50%~2%~0.4%0%
Polymarket Global~4.7%Liquid0.75%~2%~0.4%0%
Kalshi~8.0%Liquid3.50%~2%~0.5%2.00%
Polymarket US~7–8%Medium2.50%~4%~0.8%0%
Kalshi~12–15%Medium3.50%~3–4%~1%2.00%
Polymarket~17–20%+Illiquid0.75–2.50%~9%+~3.5%+0%

Friction = (fair profit − actual profit) ÷ stake × 100, on a winning $100 bet at ~50% probability. Every cost component is included: fees, overround, slippage, settlement charges. Nothing hidden.

Source: April 2026 live platform screenshots + published fee schedules (polymarketexchange.com/fees-hours.html, kalshi.com/fee-schedule). Overround measured as sum of buy-side ask prices for all outcomes on the same market. Self-check: open any sports market on Polymarket or Kalshi, add both sides' ask prices, compare to 100¢. The gap is the overround on top of the advertised fee.

Why Even Steven wins on friction

Polymarket Global advertises 0.75% for sports. Our fee is 2.00%. On a single-cell comparison we look more expensive. On total friction we win every row.

  1. No order book = no overround. When you buy "Yes" on one side and someone buys "Yes" on the other, the two prices sum to more than $1.00. That excess — typically 1–9¢ depending on liquidity — goes to market makers. On Even Steven, pools sum to exactly 100% of stakes by construction. There is no gap because there is no order book and no market maker.
  2. Flat fee regardless of size = no slippage. A $10,000 bet on Polymarket walks the order book from 50¢ to 51¢ as it fills. On Even Steven, a $10,000 bet and a $100 bet pay the same 2% friction. The pool absorbs any size at the current Z line — no book to walk, no price impact.
  3. Flat friction across every market. Tuesday MLS, Sunday NFL, niche hockey props — Even Steven charges 2.00% on all of them. On Polymarket, liquid NFL markets show ~5% friction while illiquid props show 20%+. Agents betting on long-tail markets — the majority of sports events by count — face dramatically higher costs on order book platforms.
  4. The fee cannot be raised on you. Polymarket raised fees three times in 2026: crypto in January, sports in February, eight more categories in March. FEE_PERCENT is set at deployment and is immutable. A strategy backtested at 2% friction stays at 2% friction across every market, every time period.

Trustless settlement.

Game results are submitted to UMA's Optimistic Oracle V3 via assertTruth(). UMA's optimistic model assumes the assertion is correct unless someone disputes it with a bond during the ~2-hour liveness window.

The dispute window is a feature, not a bug: anyone with evidence the result is wrong can challenge it on-chain. If unchallenged, it finalizes automatically and payouts open.

The owner cannot override outcomes. settle() was removed entirely in v1.8. Finality is enforced in code, not promised in docs.

What the owner can and can't do.

The owner runs a market's lifecycle. They can pause it (no new bets or settlement while paused), close betting early, or cancel it. Cancelling puts the market into refund mode: every stake comes back in full. The 2% fee is not refunded — it was collected when you bet.

The owner cannot:

  • — Change the outcome. settle() was removed in v1.8. Only UMA's oracle decides the result, and anyone can call executeSettlement() to apply it.
  • — Change the fee. FEE_PERCENT is set when the market is deployed and can never change.
  • — Touch the pool. The owner can recover stray USDC with recoverStuckBond(), but the code caps it at the contract balance minus the pool. Staked money is out of reach.
  • — Stop you from claiming. The claim functions ignore pause. If you're owed money, you can withdraw it, paused or not.

One backstop doesn't need the owner at all: if a closed market goes 7 days without a settlement assertion, anyone can call triggerRefund() to return every stake.

Claims stay open for 90 days. After that, the owner can sweep any unclaimed balance with sweepUnclaimed().

Verify everything.

  • Factory contract on BaseScan — source verified, immutable.
  • GitHub (MIT license) — all contract code is public.
  • Six audit reports (Claude Opus, March–September 2026): three full reviews and three delta audits of later releases, most recently v3. All critical and high findings resolved. Not a formal third-party audit.
  • getMarketEV(stake, side) — call it on any live market to check the math. This app computes the same formula locally from getMarketState() and protocolSeedTotal() to save a network call. The numbers match the contract.

Edge cases.

Unbalanced pools
Early in a market's life, one side may have more money than the other. Payouts float until the pools balance. Your line and payout estimate are shown at bet time via getMarketEV — they lock the moment you confirm the transaction.
Protocol seed
Each market is seeded with 1 USDC per side at deployment. This ensures getMarketEV returns a meaningful number even before any bets are placed.
Cancellation
If a game doesn't take place, the owner cancels the market with cancelMarket(). Cancelling can't pick a winner — it only switches the market to refund mode. Every stake is returned in full. The 2% fee is not, since it was collected when you bet. You have 90 days to claim.
7-day settlement backstop
If a closed market goes 7 days without a settlement assertion, any wallet can call triggerRefund(). Your stake is refunded in full. The 2% fee paid at placement is not refunded — it was already collected when you bet. Designed to protect bettors if a settlement bot fails.

Your funds can't get stuck.

Three recovery paths exist as structural guarantees in the contract. Every path ends in a payout or a stake refund. Nothing can be permanently locked.

  1. 1

    Settlement → claim

    Normal path. After the game, anyone calls requestSettlement(finalSpread) with a USDC bond. After UMA's 2-hour liveness window, executeSettlement() finalizes the result. Winners call claimAllPayouts() — or anyone can claim for a winner with claimPayoutFor(bettor, betIds), which always pays the bettor. 90-day claim window after settlement.

  2. 2

    triggerRefund() — anyone after 7 days

    If a closed market goes 7 days without a settlement assertion, any wallet can call triggerRefund(). Full stake returned; the 2% placement fee is not refunded (it left the contract when you bet). Use canTriggerRefund() to check availability.

  3. 3

    sweepUnclaimed() — protocol after 90 days

    After 90 days post-settlement or cancellation, any unclaimed funds are swept to the protocol wallet. Bettors have 90 days to claim — sufficient for any automated agent. The sweep is a hygiene function, not a trap: you have a 90-day window before it applies.

Source: SportsbookMarket.sol — cancelMarket(), triggerRefund(), sweepUnclaimed()

Gasless for agents (v3).

On markets created by Factory v1.6, betting needs only USDC. Claiming is gasless through POST /api/claim or the claim button, and depends on the relay being up. If the relay is down, claim directly with a little ETH.

Betting. The bettor signs a USDC transfer authorization (EIP-3009). A relay submits it with placeBetFor() and pays the gas. The bet is recorded to the signer, never to the relay. Ordinary wallets — including MetaMask accounts upgraded with EIP-7702 — use placeBetFor(). Smart-contract wallets whose signatures don't fit the standard 65-byte format use placeBetForWithSignature(), and USDC verifies them through ERC-1271.

Collecting. Anyone can submit claimPayoutFor(bettor, betIds). No signature is needed, because the payout can only go to the bettor recorded on the bet. The relay pays the gas and never touches the money. Claiming is gasless through POST /api/claim or the claim button, and depends on the relay being up. If the relay is down, claim directly with a little ETH.

Verified contracts.

Base Mainnet — source verified on BaseScan, Exact Match.

ContractAddress
SportsbookFactory v1.60x5906370b…b45390
MarketDeployer v1.10xb86d2911…1257c2
SportsbookFactory v1.5 (superseded)0xf69d4c98…00adbd
USDC (Circle)0x833589fC…A02913
UMA OOV30x2aBf1Bd7…AF500c

Deploy tx: 0x06581affd7535a03…

Base Sepolia (testnet)

USDC (Circle testnet)0x036cbd53…3dcf7e
UMA OOV30x0F7fC5E6…388deE

Markets deploy per game via the factory. Use getOpenMarkets() to discover active markets.

Annex

For developers & agents.

Even Steven is designed for AI agents. Every market exposes machine-readable state through view functions. No UI required. Full reference: AGENTS.md on GitHub.

Quick Start

javascript
// 1. Find open markets
const markets = await factory.getOpenMarkets();

// 2. Evaluate a market (payouts are gross of the 2% placement fee)
const [gameId, z, gPool, lePool, tPool, isOpen] = await market.getMarketState();
const [currentPayout, liquidPayout, impliedVig] = await market.getMarketEV(stake, greaterThan);

// 3. Approve and bet (the contract pulls stake + 2% fee)
await usdc.approve(marketAddress, ethers.MaxUint256);
await market.placeBet(greaterThan, stake);

// 4. Claim after settlement
await market.claimAllPayouts();

Discovery

solidity
// Find all open markets
address[] memory markets = factory.getOpenMarkets();

// Get full snapshot of a market
(
    string memory gameId,
    bool isOpen,
    bool isSettled,
    bool isCanceled,
    int256 currentZ,
    uint256 totalPool,
    int256 spreadMax,
    int256 spreadMin,
    uint256 feePercent,
    bool refundAvailable
) = factory.getMarketInfo(marketAddress);

// Look up a market by game ID (public mapping getter)
address market = factory.marketByGameId("NFL-2026-01-15-HOME-Chiefs-AWAY-49ers");

Pre-bet evaluation

solidity
// Get current state
(
    string memory gameId,
    int256 z,          // current Z line, 4-decimal: -35000 = -3.5
    uint256 gPool,     // stakes on greaterThan side
    uint256 lePool,    // stakes on lessEqual side
    uint256 tPool,     // total pool (stakes only, fees never enter)
    bool isOpen,
    bool isSettled
) = market.getMarketState();

// Evaluate EV before betting (payouts are gross of the 2% placement fee)
(
    uint256 currentPayout,  // gross return at current pool state
    uint256 liquidPayout,   // gross return at balanced pools — exactly $200 on $100 stake
    uint256 impliedVig      // protocol fee in bps — 200 = 2%
) = market.getMarketEV(stake, greaterThan);
// Note: getMarketEV() is exact as deployed. The seed-inflated denominator
// was a v1.9 quoting bug, fixed on-chain in v1.10 and unchanged in v1.11:
// seedPerSide is read from protocolSeedTotal and excluded, and liquidPayout
// is simply stake * 2. Settlement never counted the seed in either version.
// Do not re-apply the old client-side "correction" — it now double-counts.

// Kelly criterion note:
// liquidPayout / stake = gross multiplier at liquidity (exactly 2.0 at balance)
// True cost basis     = stake * (1 + impliedVig / 10000)  // 2% fee on stake
// Opportunity check   : currentPayout > liquidPayout → early imbalance favors you
// Net EV              = (probability * currentPayout) - stake - (stake * impliedVig / 10000)

Approve and bet

solidity
// Always use max approval — Circle USDC on Base rejects exact-amount approvals.
// Max approval covers stake + 2% fee in one go.
usdc.approve(marketAddress, type(uint256).max);

// greaterThan = true  → betting finalSpread * 10000 > lockedZ
// greaterThan = false → betting finalSpread * 10000 <= lockedZ
market.placeBet(greaterThan, stake); // minimum 1 USDC = 1_000_000

Claim

solidity
// After MarketSettled event fires:
market.claimAllPayouts(); // claims all your bets in one transaction

// Or claim a specific bet by ID:
market.claimPayout(betId);

// Claim on someone else's behalf — permissionless, no signature.
// Payout always goes to the bettor recorded on the bet, never to msg.sender.
market.claimPayoutFor(bettor, [betIds]);

// 90-day claim window — after that, sweepUnclaimed() moves funds to the protocol

Gasless bet & claim (v3)

solidity
// The bettor signs an EIP-3009 USDC authorization; a relay submits it and
// pays the gas. The bet is recorded to the signer, never to the relay.
// nonce MUST be keccak256(abi.encode(salt, greaterThan)) — binding the
// authorization to one side of the line. A random x402-style nonce is
// rejected with BadAuthorizationNonce(). Signing details: AGENTS.md.
market.placeBetFor(bettor, greaterThan, stake, auth);

// Same, for smart-contract wallets whose signature isn't 65-byte ECDSA.
// USDC verifies it via ERC-1271. EIP-7702 accounts such as MetaMask's sign
// standard 65-byte signatures and use placeBetFor() above.
market.placeBetForWithSignature(bettor, greaterThan, stake, auth);

// Collecting needs no signature at all. Payout goes only to the bettor.
market.claimPayoutFor(bettor, [betIds]);

Events & errors

solidity
// v3 adds two events. Both are additive — nothing existing changed.
event BetClaimed(address indexed bettor, uint256 indexed betId, uint256 payout);
event SettlementDetails(uint256 distributable, uint256 winningStakes);

// BetClaimed fires once per claimed bet, on every claim path.
// SettlementDetails publishes the two inputs to the payout formula:
//   payout = stake * distributable / winningStakes   (refund mode: payout = stake)
// Together with BetPlaced, any payout can be computed from logs alone.

// v3 also replaces two inherited OpenZeppelin require-strings with custom
// errors, so a revert is machine-readable:
error MarketPaused();  // was "Pausable: paused"
error NotOwner();      // was "Ownable: caller is not the owner"

Full function reference, strategy notes, and event subscriptions: AGENTS.md. Protocol version: v1.11. Six audit reports by Claude Opus, March–September 2026: three full reviews and three delta audits of later releases. Not a formal third-party audit.