Skip to main content
MmClient is the SDK surface for the Path-B market-maker rail — the ceiling of price. An MM never has to win flow: the always-on cvAMM pool clears every position regardless. An MM wins flow only by genuinely beating the pool, and the router gives the LP the cheaper of {pool, MM} while never reverting on a bad MM quote (routing). MmClient is the typed layer that makes competing on price mechanical: read the floor you must undercut, price a market, sign a firm quote that the contract will accept, and publish it. The whole MM workflow is sign + read + cancel — MmClient never sends a fill (the LP does that on-chain) and never holds collateral (a separate UnderwriterVault flow does — see below). The one write it owns is cancelNonces.

Path B in the protocol

The on-chain createSwap gate, the 13-field quote, and the three no-last-look protections.

MaxIL — the unit of risk

Why MaxIL fungibility makes an MM quote per-market, never per-NFT.

Quote signing reference

The canonical EIP-712 domain, types, and type-string.

Routing

createSwapRouted — cheaper of {pool, MM}, falling back to the pool on a bad quote.

Construction

MmClient is wired by the one-call factory. It is the only surface that receives the EIP-712 chainId, and it defaults coreAddress = core.inflexionCore, npmAddress = npm, chainId = 421614. It caches loadParams and per-market configs — both immutable after market freeze.
The raw-key path of signQuote builds a viem account from the private key and is server-side only — never put a private key in the browser. In a browser MM, sign with the connected wallet via walletClient.signTypedData and enforce the I10 + below-pool guards yourself — see browser (wagmi) signing.

Why an MM quote is per-MARKET, not per-NFT

This is the load-bearing idea behind the whole surface. MaxIL is both (a) the load-bearing payout cap and (b) the unit of risk: pure geometry, frozen at creation, identical across durations, and L-independent in the fair-rate sense. That makes positions fungible to an underwriter within a market. So an MM expresses appetite once, per marketId, as a triple — never per position: quote  =  (loadBps⏟price,  [minMaxILRatioBps, maxMaxILRatioBps]⏟appetite band,  maxNotionalV0⏟capacity)\text{quote} \;=\; \bigl(\underbrace{\text{loadBps}}_{\text{price}},\; \underbrace{[\text{minMaxILRatioBps},\, \text{maxMaxILRatioBps}]}_{\text{appetite band}},\; \underbrace{\text{maxNotionalV0}}_{\text{capacity}}\bigr) The SignedQuote carries no tokenId. Any LP position in that market whose own ratio ratioBps=maxIL⋅104/V0\text{ratioBps} = \text{maxIL}\cdot 10^4 / V_0 falls inside the band can fill the quote, up to the declared capacity. Because the price the MM sets is a load over the on-chain FairPremium (never a raw dollar premium), one geometry-independent number — the load to beat — is all the MM needs to compete. That number is the first-class streamable signal below.

Method reference

Exported nonce + bps utils:

The pool load-to-beat — the first-class signal

getPoolLoadToBeat(marketId) returns the pure pool floor an MM must undercut, with no position geometry at all — exactly because the load (a percentage spread over FairPremium) is geometry-independent. Only the dollar premium scales with a/b/maxIL; the load you compete on does not. The read is one multicall of ConvexityVault.inventory() + VolOracle.sigmaRef, finished by the parity-locked TS port of the load stack. The result PoolLoadTick carries the decomposed load so an MM can see where the pool floor comes from: totalLoad  =  baseLoad⏟σref regime band  +  utilSkew⏟utilisation, convex above the knee  +  dispSkew⏟HHI dispersion(clamped to maxLoadBps=I10)\text{totalLoad} \;=\; \underbrace{\text{baseLoad}}_{\sigma_{\text{ref}}\text{ regime band}} \;+\; \underbrace{\text{utilSkew}}_{\text{utilisation, convex above the knee}} \;+\; \underbrace{\text{dispSkew}}_{\text{HHI dispersion}} \quad\text{(clamped to } \text{maxLoadBps} = \text{I10})
To watch many markets at once, streamPoolLoadToBeat polls each market’s tick on a 4 s default interval and hands you a StreamHandle to stop:
The load-to-beat is geometry-free by design — it is the one number an MM needs to decide whether to compete in a market at all, before ever looking at a specific LP’s NFT. That is the mechanical face of MaxIL fungibility: appetite, not position, is what the MM expresses.

Pricing a market — getMarketPricing

When you do want the dollar figures (e.g. to surface a $ pool premium next to your $ quote), getMarketPricing(marketId, geometry) resolves the full pricing row in one multicall: FairValueOracle.fairPremium + ConvexityVault.inventory
  • VolOracle.sigmaRef, then finishes the load stack with the TS port. The Φ-sum fairPremium is read on-chain, never reimplemented — only the deterministic load stack is mirrored off-chain (it is parity-locked because the deployed CvammPricing lib is delegatecall-only, the cvamm port).
PricingGeometry input:
bigint
a=(Pa/P0)2a = (P_a / P_0)^2 in WAD — the lower range bound as a squared price ratio.
bigint
b=(Pb/P0)2b = (P_b / P_0)^2 in WAD — the upper range bound as a squared price ratio.
number
The market tenor; selects the duration the FairValueOracle prices.
bigint
The position MaxIL in numéraire raw (dUSDC, 6 decimals) — scales the dollar premium.
Address (optional)
The σ_ref token; defaults to the market’s oracle token.
MmMarketPricing result carries fairPremium, fairRateWad, sigmaRefWad, poolPremium, maxIL, the LoadBreakdown (load), totalLoadBps, the per-component baseLoadWad/utilSkewWad/dispSkewWad, util, conc, and regime. The pool premium is the same expression the on-chain _pricePathA uses: poolPremium  =  min⁡ ⁣( ⌈fairPremium⋅(1+totalLoad)⌉,  MaxIL)\text{poolPremium} \;=\; \min\!\Bigl(\, \bigl\lceil \text{fairPremium}\cdot(1 + \text{totalLoad}) \bigr\rceil,\; \text{MaxIL} \Bigr)
If you only have a tokenId, use getPositionGeometry(tokenId, durationSeconds) — it reads NPM positions, derives Pa/Pb from ticks, pins P0 from the oracle, and returns a { priceable, geometry, marketId, config } envelope (or a { priceable:false, reason } degrade on an oracle revert). Geometry is not what you sign, though: you sign a per-market quote. Geometry only helps you display $ figures.

Signing an EIP-712 SignedQuote

The single source of truth for the wire format is re-exported via @inflexion/sdk and @inflexion/sdk/quote. The field order, types, and domain ("Inflexion", "1") must match InflexionCore.SIGNED_QUOTE_TYPEHASH exactly, or createSwap / createSwapRouted rejects the signature. The full canonical reference lives at quote signing; the essentials: Domain:
The 13 fields, in struct order:
In TS, quotePrice, maxNotionalV0, validUntil, nonce are bigint; loadBps, the *Bps fields, and model are plain number (< 2⁵³, safe). On the wire (engine WS / JSONL logs) bigints become decimal strings via encodeQuote/decodeQuote — convert back to bigint before re-signing or verifying.
CollateralModel.FULL = 0 is the only supported mode in v1. PARTIAL = 1 exists in the enum but the contract reverts on it.
  • Limit: you cannot quote model: 1 (PARTIAL).
  • Why: PARTIAL collateral parameters (first-loss size, floor curve, liquidation buffer) are gated on the Monte-Carlo calibrated risk-parameter set and are never hardcoded — the exact failure the audit flagged.
  • Path forward: always sign model: 0, partialRatioBps: 0. PARTIAL is roadmap, gated on the quant.

(a) Key-based signing via mm.signQuote (the I10 + below-pool guards)

mm.signQuote(privateKey, quote, opts?) delegates the actual EIP-712 signing to the engine signer (never reimplemented) and enforces two guards before returning the envelope:
1

I10 — the price cap

loadBps ≤ loadParams.maxLoadBps, else it throws MmQuoteError('i10-exceeded'). This is invariant I10: the MM can never out-load the protocol’s price cap.
2

Below-pool — you only win by beating the floor

loadBps < live pool-load-bps (read via getPoolLoadToBeat), else MmQuoteError('not-below-pool'). If the pool load is unreadable, the check is skipped and reported in the result — unless you pass requirePoolCheck: true, in which case it throws MmQuoteError('pool-load-unreadable').
The guards encode the rail’s economics: a quote that is not below the pool floor can never win the router (the router requires a strict beat — routing), so signing one is pointless; and a quote above the I10 cap would revert on-chain anyway. signQuote makes those failures loud and early, off-chain, instead of a silent on-chain fallback. SignQuoteResult: { envelope, belowPoolChecked, poolLoadBps?, i10Ok: true }. The error codes are MmQuoteError.code ∈ { i10-exceeded, not-below-pool, pool-load-unreadable, no-signer, bad-nonce }.

(b) Browser (wagmi) signing

No private key in the browser. Sign with the connected wallet, then enforce the same two guards yourself — signQuote’s raw-key path is server-only:
Same digest, different signer. Both paths produce a signature over the identical EIP-712 digest the contract computes (quoteDigest). The browser path simply uses the wallet to sign and replicates the guards in app code, because there is no key for signQuote to load. EIP-1271 contract signers are verified on-chain only — the off-chain verifyQuote is the EOA path.

Publishing — quoteStream and the engine transport

MmClient deliberately does not depend on ws — it never opens a socket. You inject the transport. quoteStream(options) runs the firm-quote / short-TTL loop the engine is built around: each tick reads getPoolLoadToBeat, calls your build(poolLoad) (return undefined to skip a tick), signs via signQuote (re-enforcing I10 + below-pool), then hands the envelope to your injected publish transport. It degrades silently per tick and returns a StreamHandle.
The engine verifies the signature + a light freshness gate and stores the cheapest currently-valid quote per market (lowest loadBps, not expired). An LP’s previewPremium then reads GET /quote?marketId=… for that cheapest load. The engine returns a load, not a dollar premium — the dollar premium is position-specific and derived on-chain at createSwapRouted as FairPremium⋅(1+loadBps)\text{FairPremium}\cdot(1+\text{loadBps}). Full protocol + endpoint detail: the engine and API endpoints.
A leaked signed quote is a bearer instrument — but it is not a pickoff risk.
  • Limit: once you sign, the bytes can be replayed by anyone who copied them, beyond the engine’s cancel.
  • Why it is safe: three deterministic on-chain protections, no last-look — the oracle-anchored price band (I9 / Fork 2) auto-voids the quote if spot drifts past priceBandBps; the short [5,15]s validity window bounds the leakage interval; and the bitmap nonce lets you kill exactly one quote on-chain.
  • Mitigation: keep TTL short (re-issue every 5 s as above) and cancelNonces the moment you withdraw a quote. Detail: the three protections.

The book, fills, and capacity

getBook — your open positions, scanned on-chain

getBook(mm, opts?) scans Core.nextSwapId → swaps and returns the MM’s BookResult (the swaps it underwrites), optionally with per-swap greeks (withGreeks: true) computed by finite-differencing the deployed ILMath.computeIL. greeksForSwap(swapId) does the same for a single swap over ±1% P0. This works today, with no subgraph — it is a direct on-chain scan.

Fill attribution is COARSE today

On-chain, fill attribution is coarse — isQuoteFilled cannot tell filled from cancelled.
  • Limit: isNonceUsed(mm, nonce) is set on both a fill and a cancel, and SwapCreated / SwapRouted carry no quoteId / nonce. So isQuoteFilled(mm, nonce) returns NonceStatus { spent, precision: 'coarse', detail } — never a precise filled-vs-cancelled distinction.
  • Why: the on-chain settlement core stays minimal; the precise attribution belongs in the indexer, not the hot path.
  • Path forward: the precise path is the QuoteFilled event (swapId, mm, quoteId, nonce, loadBps), emitted since the 2026-06-05 deploy. It is now queryable — the subgraph is deployed and indexing it, served live via the hosted API. The on-chain watchFills still polls SwapCreated logs by mm (works now, coarse) as the no-backing fallback. If SUBGRAPH_URL is unset, history degrades to a typed pending state, never an error.
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.

Capacity — capacityRemaining (I7)

A signed quote can fill repeatedly only up to its declared maxNotionalV0. The on-chain ledger is the authority — invariant I7: remaining  =  max⁡ ⁣(0,  maxNotionalV0−consumedNotional[quoteId])\text{remaining} \;=\; \max\!\bigl(0,\; \text{maxNotionalV0} - \text{consumedNotional}[\text{quoteId}]\bigr)
consumedNotional[quoteId] is incremented atomically in the on-chain effects phase before any external call, so concurrent fills cannot over-consume a quote (protocol detail: Path B capacity authority).

Selective cancel — cancelNonces

cancelNonces(nonces) is the one write MmClient owns — it flips exactly the bitmap bits you pass, never cancel-all, so you revoke precisely the quotes you want. It throws a clear no-signer error if no wallet is wired. For an external signer, buildCancelNoncesTx(nonces) returns the unsigned { to, data, value }.
A cancelled bit can never fill — it satisfies the second clause of I7. This is the on-chain backstop behind the engine’s off-chain cancel: even if a quote is still in someone’s hand, it is dead the instant you cancel its bit.

MM collateral — the UnderwriterVault flow

Collateral is not part of signQuote. Before any of an MM’s quotes can be filled, the MM posts collateral to its own per-MM UnderwriterVault via a separate ERC-20 approve + deposit. On a Path-B fill, the contract requires availableBalance(mm) ≥ maxIL and locks maxIL of the MM’s own collateral; at settlement the LP is paid min(realized_IL, MaxIL) from that locked collateral — never protocol funds. The standing invariant is I5: locked[mm] ≤ deposited[mm] by construction. MmClient reads the collateral state (it does not write the deposit):
The 2026-06-05 redeploy ran a real routed fill that chose the MM: the router saw a cvAMM premium of $13.80 vs the MM’s $8.93 (MM loadBps 1000), locked MaxIL $3,215.65 from the MM’s own UnderwriterVault, and at expiry paid the LP $245.66 from that collateral — not protocol funds.

End-to-end worked example — stream the floor, sign, publish

This is the canonical MM loop: read the geometry-free price-to-beat, sign a per-market quote that undercuts it (passing the I10 + below-pool guards), and publish it to the engine. The chain re-checks everything; an LP then fills the cheaper of {pool, your quote} via createSwapRouted.
What each step buys you, restated as the rail’s economics:
1

Geometry-free floor

quoteStream reads getPoolLoadToBeat every tick — the one number that decides whether to compete, no NFT needed (MaxIL fungibility in action).
2

Strict undercut + guards

loadBps is set below the live floor; signQuote re-enforces I10 (≤ cap) and below-pool (strict beat) before signing, so you never ship a quote that cannot win or would revert on-chain.
3

Firm, short-TTL publish

Each quote is GTD for [5,15]s and re-issued every 5 s — firm with no last-look, protected by the I9 band + the bitmap nonce.
4

On-chain authority

Capacity (I7) and collateral (I5) live on-chain; the engine’s view is advisory. A fill locks the MM’s own collateral and pays the LP from it at settlement.

The two separate claims

The MM rail changes neither guarantee. The settlement core enforces the LP-facing guarantee identically for 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 the underwriter — on Path B, the market maker posting collateral in its own UnderwriterVault:
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.
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

Quote signing reference

The canonical EIP-712 domain, type-string, and verify/recover helpers.

The engine

The Path-B relayer: WS protocol, GET/POST /quote, the freshness gate.

Path B in the protocol

The on-chain createSwap gate, capacity (I7), and the no-last-look protections.

The cvamm port

Why the load stack is mirrored off-chain, parity-locked, while the Φ-sum is read on-chain.

MmClient API types

The full typed signatures: PoolLoadTick, MmMarketPricing, SignQuoteResult, NonceStatus.

Quote helpers

encodeQuote / decodeQuote / quoteDigest / verifyQuote signatures.