Protocol documentation

A short memory for a precise market question.

Markout asks what the pool's price did after a trade. If the move reverted, the premium returns. If it held, active liquidity receives it. This is the mechanism from first swap to terminal verdict.

The problem: one-shot arbitrage

MEV-protection hooks that analyze continuation flow miss the most dangerous case: an arbitrageur that executes exactly one trade to snap the AMM to the global price and leaves. There is no continuation to analyze. So Markout doesn't look at what the trader did — it looks at what the pool's price did after them.

Informed flow moves the pool to the global price and it stays. Uninformed flow moves the pool away, and natural arbitrageurs push it back within seconds.

The premium, payable through any router

Every swap fills immediately at the pool's 3 bps fee. On top, the hook charges a reversion-insurance premium — a live-quoted bps rate of the realized input taken from the post-swap balanceDelta, never a slot0 estimate, for exact-in and exact-out alike. Dust swaps whose bond would round to zero revert with SwapTooSmall.

The charge rides v4's hook-delta mechanism: for exact-in swaps the hook returns it on the specified delta in beforeSwap; for exact-out swaps on the unspecified delta in afterSwap. Either way it lands in the swap caller's own PoolManager delta — so any router that can settle a normal v4 swap (Universal Router, your own contract, v4's test routers) pays it with zero Markout-specific code. No allowlist, no settleFor, no initializeRouter.

premium = amountIn × premiumBps(pool) · donate +3 bps, refund −1 bps · clamp 5–60

The rate is live-quoted from this pool's own settle history: every donate verdict raises it 3 bps, every refund verdict lowers it 1 bp, and it clamps at 5 bps (dust swaps can never bypass) and 60 bps (no runaway). The only way to pump the rate is to actually donate real premia to LPs, so the grief is self-funding for the pool — poke spam and token gifts cannot move it. premiumQuoteFor is the exact rate the next swap charges.

The premium is a deterrent tax on one-shot toxicity, not a complete LVR hedge. Slow trend that never reverts inside the window still leaves LPs with inventory risk. Dust donations can walk the quote toward 60 bps; refunds move it back one basis point at a time.

The 24-second fixed window

Each trade records its own immutable window [bondTime, bondTime + 24 s]. The hook maintains a previous-tick accumulator (Uniswap-V2-style attribution: elapsed time accrues to the tick held before each update), stored as an append-only history that nothing ever prunes. Settlement computes the window's average tick by binary-searching that history — so settling at window close, an hour late, or after heavy churn produces the identical verdict, and permissionless pokes or later swaps can never freeze escrow.

24 s ≈ two 12 s blocks: a full 1:1 reversion landing one block after the trade sits exactly on the 50% frontier and refunds — no overshoot needed.

T = 24 s · avgTick = (Σ tick_held × Δt) / T over the immutable window

The 50% reversion frontier

The classifier is normalized: it compares the reversion to the trade's own impact, measured in tick space, not to an absolute bps band. A 2-tick trade and a 2000-tick trade face the same frontier.

impact = post − pre · residual = avg − pre · refund ⟺ residual × 2 ≤ impact

Zero-impact trades refund. Overshoot past pre refunds. Movement further away donates. In plain English: if at least half of your own price impact came back, you were organic and the bond returns — in the settlement transaction itself.

An atomic spot sandwich remains an honest boundary: the front leg and victim can refund because the backrun restores price; the unreversed backrun leg is what donates. A same-block rule cannot distinguish that front leg from a legitimate one-block reversion.

Outcomes 1 / 2 / 3

Settlement is permissionless — anyone calls settle(tradeId) after the window — and terminal: the verdict is recorded before any value moves, and settle credits in-range LPs in the same transaction whenever liquidity exists. Permissionless does not mean automatic: someone still has to send the settlement transaction.

outcome 1 · Refunded

The oracle said refund and the token delivered. The bond is paid to the trader inside the settlement transaction. No claim transaction exists on this path.

outcome 2 · RefundPending

The oracle said refund but delivery failed (blacklist-style tokens). The verdict survives; claimRefund retries it, marking claimed before the transfer so reentrancy and replay are impossible. A failed retry resets and stays retryable — settlement can never brick.

outcome 3 · Donated

The price sustained. The premium is forfeited — and whenever the pool has active liquidity, in-range LPs are credited inside the settlement transaction itself through v4's donate(). Only at zero liquidity does the value wait in a per-pool pending bucket for the permissionless flushDonation(poolId); the settle itself still succeeds and can never brick. Credit goes to whoever is in range at settlement or flush time, not specifically to the LPs who carried inventory through the original move.

The opt-in batch lane

The SAME 24-second clock runs a second, opt-in lane. A trader enqueues one side of a 24 s epoch by calling placeBatchOrder directly — no router needed — and the full deposit moves into explicit custody in the hook, cancellable any time before the epoch clears. When the epoch ends, anyone calls clearBatch: opposing orders net at the epoch's accumulator TWAP, the dust-bounded residual executes as one normal bonded spot swap (through an immutable hook-owned child router — v4-core skips hook callbacks on self-calls, so the hook cannot dodge its own premium lane), and every order on a side fills at the same uniform price: the TWAP clamped by realized execution, so the hook never subsidizes a fill.

Honest limits of the batch lane: a lone order in an empty epoch is a one-epoch TWAP fill — not a CoW-style auction. Cancelled orders cannot move the TWAP (the price is time-weighted, not book-ordered). Unmatched leftover still clears as one bonded residual with an unbounded price limit — that path is a spot swap and can be sandwiched like any other. Two-sided exact nets never hit the curve. Clearing late is identical because the epoch TWAP is immutable in the append-only accumulator.

Why any router works

Because the premium is part of the swap caller's own delta, a router settles it the way it settles every other wei it owes: pay your own delta, done. The provided MarkoutRouter is convenience, not a gate — it adds a deadline, exact-in minimum output, exact-out maximum input (bond included), strict transfer checks, and native support, and declares the human beneficiary in hookData so refunds route to the end user.

The beneficiary rule is exact: only a 32-byte hookData holding a nonzero address declares a beneficiary. Empty payloads, arbitrary-length payloads, and a zeroed 32-byte word all fall back to the direct swap caller — no revert mid-swap, and no refund can ever be sent to address(0). Universal Router integrators: pass exactly abi.encode(endUser) as hookData; a router that declares nothing receives its own refunds.

swapDelta(caller) = poolDelta − hookDelta(bond) · settle your delta, that's it

Try it

Hosted at markout-nine.vercel.app — best in Chrome desktop with MetaMask (or any injected wallet) on Sepolia. The app runs the whole loop against the live deployment, with one-click deterministic demos for both verdicts; the landing page streams the live tape with no wallet at all. The terminal runbook in demo.md is copy-pasteable on the current ABI — including the pre-signed back-to-back swap pair that lands the reversion one block after the buy. Demo video: coming, human-recorded.