/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.
Path B — the MM rail
MaxIL — the unit of risk
Quote signing in depth
The MmClient SDK surface
Before you start — prerequisites
Connect a wallet on Arbitrum Sepolia (chainId 421614)
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.Hold dUSDC — the numéraire collateral asset
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.(Optional) Have the engine running
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.Section 1 — Post collateral (UnderwriterVault)
Your collateral lives in the per-MMUnderwriterVault — 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:
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 noMmClient method for collateral — it
does not invent one — so the page calls the documented UnderwriterVault functions
directly via the wallet client.
Approve (only if your allowance is short)
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.Deposit
UnderwriterVault.deposit(amount). A Max button fills your wallet balance.UnderwriterVault.withdraw(amount) — and only the
available (un-locked) portion is withdrawable; the locked part is committed to open
fills.
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.
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: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.10000 (=
100%) means “cover any ratio”. This is which positions you cover, never a price
input.FairPremium. It must satisfy both guards:
loadBps ≤ maxLoadBps (I10) and strictly below the live
pool load, or your quote will never win the route.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.consumedNotional[quoteId] enforces it across every fill.secondsAhead is bound to [5s, 15s] — a short leakage
window for a bearer signature.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:Pool load to beat — the headline you must undercut
Pool load to beat — the headline you must undercut
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.Oracle price — your quotePrice anchor
Oracle price — your quotePrice anchor
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.Illustrative pricing — a representative position only
Illustrative pricing — a representative position only
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.Representative payoff chart — your bounded downside
Representative payoff chart — your bounded downside
Why per-market, not per-NFT
The absence oftokenId 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:
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:
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.”
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:Re-read & re-enforce I10 + below-pool
maxLoadBps and pool load, re-check both guards. A stale-tab quote that
has drifted above the pool is rejected at the last instant.Read the fresh oracle price for quotePrice
quotePrice is the current oracle price, not a cached one. This is the
Fork-2 band anchor.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.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.InflexionCore.SIGNED_QUOTE_TYPEHASH exactly
or the on-chain createSwap / createSwapRouted rejects the signature. The canonical
browser pattern, mirroring what the page does:
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.
Engine offline — you still sign
Section 3 — Book, fills, and cancel
BookAndFills is your live position view.
Your book — active fills
mm.getBook(address) every 15s shows Exposure () and Locked ( == 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.
Recent fills — coarse attribution (and why)
TheFillsFeed (mm.watchFills) polls SwapCreated logs filtered by your mm
address.
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 (MMloadBps1000) and chose Path B because it strictly beat the pool.- MaxIL $3,215.65 was locked from the MM’s own
UnderwriterVaultcollateral. - At expiry: realized IL $245.66, payout $245.66 — settled from the MM’s collateral, never protocol funds.
The two separate claims
The MM rail never changes which guarantee applies to whom. The LP-facing guarantee is enforced identically on both rails: And the capital-at-risk disclosure applies to you, the underwriter on Path B:- 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
pendingstate — never an error.
421614 · deploy block 274081134 (subgraph startBlock) · deployed 2026-06-05
Core protocol
Stylus & libraries
Oracles
Uniswap
Chainlink feeds
Demo tokens / pool / LP
Lifecycle swaps
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.