Skip to main content
The /underwrite page is the Path-B market-maker rail in the live app. It is where you, an MM, post collateral, build and sign a firm EIP-712 quote, and publish it to the off-chain engine so an LP can fill it on-chain. The whole flow rests on one idea: an MM quote is per-market, never per-NFT — because MaxIL is the fungible unit of risk. You are doing the opposite of the LP on /protect: they pay a fixed premium to shed in-range IL convexity; you collect that premium for taking it. You are short convexity, paid the premium for it, and your capital is at risk in a way the LP’s is not.
Your capital is NOT guaranteed. You are short convexity and paid the premium for it. The no-bad-debt guarantee on /protect is for the protected LP — not for you. On a fill the protocol locks your own collateral at MaxIL and pays the LP min(realized_IL, MaxIL) from it at settlement. The page header states this verbatim; the full disclosure is at the bottom of this page. This is a separate claim from the LP guarantee — the two are never merged.

Path B — the MM rail

The protocol-level reference: createSwap, the 13-field quote, the three protections.

MaxIL — the unit of risk

Why MaxIL fungibility is what makes your quote per-market, not per-position.

Quote signing in depth

The EIP-712 domain, types, and the field-order rule that must match the typehash.

The MmClient SDK surface

Every read/write behind this page: pricing, pool-load-to-beat, sign, cancel.

Before you start — prerequisites

1

Connect a wallet on Arbitrum Sepolia (chainId 421614)

Every write on the page is gated behind a ConnectGate; reads work immediately. The live deployment is the 2026-06-05 full redeploy. Addresses load from the SDK, never hardcoded — see the deployment table.
2

Hold dUSDC — the numéraire collateral asset

Collateral, premiums, V0, and MaxIL are all denominated in dUSDC (6 decimals). 1 dUSDC = 1_000_000n raw. dWETH is the demo volatile token (18 dec); Chainlink feeds are 8 dec; WAD (1e18) is used for load and σ_ref ratios. See decimals and units.
3

(Optional) Have the engine running

With NEXT_PUBLIC_ENGINE_URL set, the page publishes your signed quote to the relay (POST /quote). Without it, the page still signs in your browser and renders the exact wire envelope you would hand the engine — it never crashes. See degradation.
The page has three sections, top to bottom: post collateral, build & sign a quote, and book + fills.

Section 1 — Post collateral (UnderwriterVault)

Your collateral lives in the per-MM UnderwriterVault — separate from the Path-A ConvexityVault that pooled depositors fund. A quote cannot be filled against collateral you have not posted: at fill the contract requires availableBalance(mm) ≥ maxIL of the position, else MMUndercollateralised. The CollateralPanel reads mm.getMmCollateral(address) every 12s and shows three numbers: This is invariant I5: locked ≤ deposited, by construction. Locked collateral is never re-routed; it stays instantly liquid to pay LPs at settlement.

Deposit is two transactions — and there is no SDK helper for it

Collateral deposit/withdraw is a separate, direct on-chain flow. The SDK deliberately exposes the ABI + address but no MmClient method for collateral — it does not invent one — so the page calls the documented UnderwriterVault functions directly via the wallet client.
1

Approve (only if your allowance is short)

ERC-20 approve(UnderwriterVault, amount) on dUSDC. The spender is the UnderwriterVault — not InflexionCore (the LP-premium spender) and not the ConvexityVault (the depositor spender). Three different spenders; this is the recurring gotcha.
2

Deposit

UnderwriterVault.deposit(amount). A Max button fills your wallet balance.
UnderwriterVault deposit (direct, via wagmi walletClient)
Withdraw is a single tx — UnderwriterVault.withdraw(amount) — and only the available (un-locked) portion is withdrawable; the locked part is committed to open fills.
Collateral must stay instantly liquid — never lock it elsewhere.
  • The rule: the page’s deposit hint says exactly this. It is a protocol invariant, not a UI nicety.
  • Why: your locked collateral is the only source the LP is paid from at settlement. If it were routed to a utilization-gated venue (Aave / Compound) it could be un-redeemable at the exact moment it is needed, breaking the no-bad-debt guarantee the LP relies on.
  • The path forward: idle (un-locked) capital only may ever earn yield, and only via instantly-redeemable wrappers (sDAI / tokenized T-bills). Locked collateral stays put. See collateral models.

Section 2 — Build & sign a firm quote (PER-MARKET, no tokenId)

This is the conceptual heart of the page (QuoteBuilder). A quote is per-market, not per-position. You pick a market and express a single appetite; the quote then fills any in-range position in that market whose MaxIL/V0 ratio falls in your band, up to your capacity. The contract checks the band at fill.
There is no tokenId anywhere in a quote. The SignedQuote struct carries 13 fields and not one of them is a position id. This is the entire design, not an omission — see why fungibility makes it per-market.

The inputs you set

The builder takes six inputs:
dropdown — one of 9
Picks the marketId over all 9 markets (3 fee tiers × 3 durations) and sets the oracleToken anchor your quotePrice is read against. One quote binds to exactly one market.
0 → 10000
Your appetite band: the MaxIL/V0 ratios you are willing to write. 10000 (= 100%) means “cover any ratio”. This is which positions you cover, never a price input.
≤ maxLoadBps AND < pool load
required
Your markup over the on-chain FairPremium. It must satisfy both guards: loadBps ≤ maxLoadBps (I10) and strictly below the live pool load, or your quote will never win the route.
25 → 500 on-chain
The Fork-2 band around your signed quotePrice. An LP can only fill your quote while the live oracle price stays within ± this band of your anchor. This is your firm-quote, no-last-look protection.
uint128, numéraire raw
Your capacity cap for this quote (I7). The on-chain ledger consumedNotional[quoteId] enforces it across every fill.
bound to now + [5,15]s on-chain
The validity window. On-chain secondsAhead is bound to [5s, 15s] — a short leakage window for a bearer signature.
The premium an LP ultimately pays is derived from your load over the published fair value — you never stream a raw premium: premiumB  =  ⌈FairPremium⋅(1+loadBps104)⌉,capped at MaxIL\text{premium}_B \;=\; \left\lceil \text{FairPremium}\cdot\Bigl(1 + \tfrac{\text{loadBps}}{10^4}\Bigr) \right\rceil, \qquad \text{capped at MaxIL} where FairPremium = fairRate · MaxIL is the closed-form Φ-sum read on-chain, never reimplemented.

The live reference cards (right side)

While you set inputs, four cards stream live state so you can price competitively:
mm.getPoolLoadToBeat(marketId) every 8s. The bps load the always-on pool charges, broken into Base / Util skew / Disp skew, with the regime badge. Copy: “Your load must be strictly below this and ≤ maxLoadBps (I10)”. This number is geometry-free — it is the pure pool floor, needing no position. See the load stack.
OracleManager.getPrice(oracleToken) every 12s — the Fork-2 band anchor you sign against, position-independent. Signing is blocked if it cannot be read (a stale feed / sequencer down): there is no anchor to sign, so the page refuses rather than sign against a phantom price.
Uses the seeded demo LP (tokenId 3218) as a representative position via mm.getPositionGeometry + mm.getMarketPricing to show a sample MaxIL, the on-chain FairPremium, the pool premium, and your estimated premium at your load. The label is explicit: the quote does not depend on this — each LP is priced from their own position at fill.
The same capped-payoff visual the LP sees, captioned: “You pay the LP min(realized IL, MaxIL) at expiry… the cap is what keeps your downside bounded”. The amber MaxIL line is the most you ever pay on any one fill.

Why per-market, not per-NFT

The absence of tokenId is the direct consequence of MaxIL being the fungible unit of risk. Because MaxIL is pure geometry, frozen at creation, identical across durations, and L-independent in the fair-rate sense, every position in a market becomes interchangeable to you as an underwriter. You do not need to see a specific NFT to price it — you express one appetite, once, and the contract enforces it per-fill. So your quote carries a triple: quote  =  (loadBps⏟price,  [minMaxILRatioBps, maxMaxILRatioBps]⏟appetite,  maxNotionalV0⏟capacity)\text{quote} \;=\; \bigl(\underbrace{\text{loadBps}}_{\text{price}},\; \underbrace{[\text{minMaxILRatioBps},\, \text{maxMaxILRatioBps}]}_{\text{appetite}},\; \underbrace{\text{maxNotionalV0}}_{\text{capacity}}\bigr) and any LP position in that market whose ratio falls in your band can fill it. The band is the only place the LP’s specific geometry enters the gate, and the contract computes it from the LP’s own position, not from your quote: ratioBps  =  maxIL⋅104V0,require    ratioBps∈[minMaxILRatioBps, maxMaxILRatioBps]\text{ratioBps} \;=\; \frac{\text{maxIL}\cdot 10^4}{V_0}, \qquad \text{require}\;\; \text{ratioBps} \in [\text{minMaxILRatioBps},\, \text{maxMaxILRatioBps}]
The MaxIL-ratio band is a convenience appetite filter, not an adverse-selection fix. Every v3 position parameter is public on-chain via positions(tokenId) — both rails read the exact geometry, so there is no information asymmetry to defend against. The band just lets you say “I only write 2%–7% MaxIL/V0 widths” once, and every conforming position is fungible to you. See Path B in depth.

Guard banners — you cannot sign a losing quote

Before the Sign button activates, the builder shows two guard banners:
  • an I10 banner if loadBps > maxLoadBps;
  • a “does not undercut the live pool” banner if loadBps >= poolBps — “A quote at or above the pool floor will not win the route.”
The Sign button is gated on canSign = valid inputs AND I10 OK AND below-pool AND wallet connected. This mirrors the same guards mm.signQuote enforces server-side.

Signing — browser EIP-712, no private key in the browser

On click, the page does not trust the values you set a moment ago — it re-reads fresh state and re-enforces the guards right before signing:
1

Re-read & re-enforce I10 + below-pool

Fetch fresh maxLoadBps and pool load, re-check both guards. A stale-tab quote that has drifted above the pool is rejected at the last instant.
2

Read the fresh oracle price for quotePrice

The signed quotePrice is the current oracle price, not a cached one. This is the Fork-2 band anchor.
3

Build the SignedQuote

model = FULL (0), partialRatioBps = 0, a random quoteId, and a bitmap nonce = encodeNonce(word, bit). PARTIAL is enum-only and reverts — FULL is the only mode in v1.
4

Sign with the connected wallet

walletClient.signTypedData over the domain { 'Inflexion', '1', 421614, core.inflexionCore }, SignedQuoteTypes, primaryType SignedQuote. No key ever touches the browser JS — your wallet signs.
The 13-field struct order must match InflexionCore.SIGNED_QUOTE_TYPEHASH exactly or the on-chain createSwap / createSwapRouted rejects the signature. The canonical browser pattern, mirroring what the page does:
browser quote signing (wagmi) — mirrors /underwrite

Publish to the engine

The signed envelope is POSTed to ${NEXT_PUBLIC_ENGINE_URL}/quote as a QuoteWire — bigints serialized to decimal strings via encodeQuote. The signed-envelope card prints loadBps, band, quotePrice, maxNotionalV0, validUntil (local time), model, quoteId, and nonce.
publish a signed quote to the engine

Engine offline — you still sign

If NEXT_PUBLIC_ENGINE_URL is unset, the quote cannot be published — but signing still works and nothing crashes.
  • Limit: without the engine relay, the page cannot POST your quote to an LP-facing book, so no one can discover and fill it through the app.
  • Why: Path B is the only rail that needs an off-chain quote book; Path A (the pool) is signature-free and always-on, so the absence of the engine never affects an LP’s ability to get protection.
  • Mitigation: the page shows “engine not configured” and renders the exact wire envelope you would hand the engine WS — or that an LP could pass directly to lp.buyProtection. Because EIP-712 verification is on-chain, any LP holding your valid signed quote can call createSwap directly, with no engine in the loop. The relay is a convenience, never a trust dependency. See the trust model.

Section 3 — Book, fills, and cancel

BookAndFills is your live position view.

Your book — active fills

mm.getBook(address) every 15s shows Exposure (∑V0\sum V_0) and Locked (∑MaxIL\sum \text{MaxIL} == your committed collateral, amber), plus a per-swap table of swap / V0 / MaxIL / premium / expiry. Discovery is an on-chain swap scan (no subgraph) — so aggregate exposure is exact, today.

Cancel quote nonces

Paste space/comma-separated decimal nonces ((word<<8)|bit) and mm.cancelNonces burns the bitmap bit so that quote can never fill again. This is the on-chain backstop to the engine’s cancel: even if the relay still has the bytes, a cancelled bit is dead on-chain instantly.
cancel one quote's nonce on-chain

Recent fills — coarse attribution (and why)

The FillsFeed (mm.watchFills) polls SwapCreated logs filtered by your mm address.
Fill attribution is coarse today — you see that you filled, not which quote filled.
  • Limit: the live SwapCreated event carries the mm address but not the quoteId or nonce, so a fill cannot be joined back to a specific signed quote. The badge in the UI says “coarse”.
  • Why: isNonceUsed(mm, nonce) is also set on both fill and cancel, so it cannot by itself distinguish a fill from a cancellation.
  • Mitigation / path forward: the deploy already emits the precise QuoteFilled event (swapId, mm, quoteId, nonce, loadBps) on-chain since block 274081134. Precise per-quote attribution becomes available the moment the subgraph indexes it — it degrades to a typed pending state in the meantime, never a fake number. Your aggregate exposure and locked collateral are exact today regardless.

Proven live on Path B

The 2026-06-05 redeploy ran a real routed fill that chose the MM:
  • createSwapRouted (swap #2) saw the pool at $13.80 vs the MM at $8.93 (MM loadBps 1000) and chose Path B because it strictly beat the pool.
  • MaxIL $3,215.65 was locked from the MM’s own UnderwriterVault collateral.
  • At expiry: realized IL $245.66, payout $245.66 — settled from the MM’s collateral, never protocol funds.
This is the rail end to end: you win by undercutting the pool, your own collateral backs the fill, and the LP is paid from it.

The two separate claims

The MM rail never changes which guarantee applies to whom. The LP-facing guarantee is enforced identically on both rails:
LPs are always paid with no bad debt in FULL mode — qualified: capped payoff + solvent USDC + oracle/settlement liveness.The no-bad-debt guarantee is exact only under the full clause:
  • FULL collateralization — the underwriter has posted exactly MaxIL per swap, locked for the life of the position.
  • Capped payoff — the payout is min(realized_IL, MaxIL). The cap is load-bearing: it is what makes payout ≤ collateral hold by construction.
  • Solvent USDC — the collateral numeraire (dUSDC) holds its peg and remains redeemable.
  • Oracle / settlement liveness — the price oracle stays live and settle() runs within the protocol’s settlement window.
  • No rehypothecation breach — locked collateral stays instantly liquid; it is never routed to utilization-gated venues.
Stated without these qualifiers, the guarantee is not correct.
And the capital-at-risk disclosure applies to you, the underwriter on Path B:
Depositor capital is NOT guaranteed: the junior tranche is first-loss; the senior tranche is structurally protected from underwriting loss only, not systemic tail.Underwriting capital — whether supplied to the pooled cvAMM vault (Path A) or posted directly by a market maker (Path B) — is at risk:
  • Junior tranche — first-loss. Junior depositors absorb underwriting losses first and buffer the senior tranche.
  • Senior tranche — protected from underwriting loss, exposed to the systemic tail. The senior tranche is structurally shielded from ordinary underwriting loss while junior capital buffers, but it still takes the systemic tail once the junior buffer is exhausted.
No-bad-debt is a guarantee to the LP being paid (see the FULL-mode payout clause), not a guarantee that depositor or MM capital is preserved.
Structures ship from day one; the full DYNAMICS (pool-vs-MM spread as a forward-vol signal) mature with multiple competing MMs and volume. The dataset begins at the single redeploy; the off-chain telemetry halves begin at the first interaction logged.What Inflexion offers today is the architecture and the first public view into the microstructure of the DeFi LP volatility-risk premium — not yet a mature dataset:
  • Structures are present day-one. All five non-circular behavioral signals are defined and queryable from launch.
  • Dynamics mature with volume. The richer signal — the pool-vs-MM spread read as a forward-vol indicator — only sharpens once multiple market makers compete and real flow accumulates.
  • The dataset begins at the single redeploy, and the off-chain telemetry halves begin at the first interaction logged. History is served live from the subgraph via the API; if a backing (subgraph URL / RPC / telemetry) is absent, surfaces degrade to a typed pending state — never an error.
Network: Arbitrum Sepolia · chainId 421614 · deploy block 274081134 (subgraph startBlock) · deployed 2026-06-05

Core protocol

Stylus & libraries

Oracles

Uniswap

Demo tokens / pool / LP

Lifecycle swaps

Live create→settle lifecycle (2026-06-05, short 300s fee-500 market 0xb8bbd684f213d5833886ade7b531a6949d85522249881a2b5d46a5cc76e439c2):
  • Path A (cvAMM convexity) — swapId 1, LP tokenId 3220 · V0 $270,531.28 · MaxIL $1,669.24 · premium $9.70 (0.58% of MaxIL) · settled payout $148.64 from ConvexityVault.
  • Path B (MM routing) — swapId 2, LP tokenId 3221 · MM beat cvAMM ($8.93 vs $13.80) · MaxIL $3,215.65 · MM collateral locked $3,215.65 · settled payout $245.66 from MM collateral.

Where to go next

Path B — the protocol reference

createSwap, all 13 quote fields, capacity (I7), nonce replay defense.

Quote signing

The exact EIP-712 domain, types, and typehash your signature must match.

Hedging your short convexity

Greeks + the three-leg hedge suggester — your residual-risk choice, not pool solvency.

Earn passively instead

Underwrite via the pooled cvAMM (Path A) — signature-free, no quote book.