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, permarketId, as a triple — never per position:
The SignedQuote carries no tokenId. Any LP position in that market whose
own ratio 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:
streamPoolLoadToBeat polls each market’s tick
on a 4 s default interval and hands you a StreamHandle to stop:
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 Φ-sumfairPremiumis read on-chain, never reimplemented — only the deterministic load stack is mirrored off-chain (it is parity-locked because the deployedCvammPricinglib is delegatecall-only, the cvamm port).
PricingGeometry input:
bigint
in WAD — the lower range bound as a squared price ratio.
bigint
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:
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:
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.
(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').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.
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
. Full protocol + endpoint detail:
the engine and API endpoints.
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
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
pendingstate — 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:
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 }.
MM collateral — the UnderwriterVault flow
Collateral is not part ofsignQuote. 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.
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: And the capital-at-risk disclosure applies to the underwriter — on Path B, the market maker posting collateral in its ownUnderwriterVault:
Network: Arbitrum Sepolia · chainId 421614 · deploy block 274081134 (subgraph startBlock) · deployed 2026-06-05
Core protocol
Stylus & libraries
Oracles
Uniswap
Chainlink feeds
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.