Skip to main content
A SignedQuote is the firm, EIP-712-signed unit a Path-B market maker streams to the engine so it can compete on price against the always-on cvAMM pool. This page is the authoritative, language-agnostic definition: get the domain, field order, types, and the typehash pre-image byte-identical, and a quote signed in any language verifies on-chain. Get any of them wrong and createSwap / createSwapRouted rejects the signature.
The single source of truth is re-exported through @inflexion/sdk and @inflexion/sdk/quote. The TS field order, types, and domain must match InflexionCore.SIGNED_QUOTE_TYPEHASH exactly — and the engine’s SIGNED_QUOTE_TYPE_STRING is byte-identical to it. A quote is per-market, never per-NFT — the SignedQuote carries no tokenId. That is load-bearing for the whole model and is explained in the two rails and on Path B; the short version is repeated in Why per-market below.

The EIP-712 domain

The domain is fixed and the four fields are non-negotiable:
string
required
Literal "Inflexion". Hashed into the domain separator.
string
required
Literal "1". The protocol’s signing version, not the package version.
uint256
required
421614 (Arbitrum Sepolia). In TS this is the bigint 421614n. The SDK reads it from CHAIN_ID; the engine defaults it from the deployment registry, never hardcoded.
address
required
InflexionCore — the state machine that calls _hashTypedDataV4, not a vault. The engine defaults it from the registry (VERIFYING_CONTRACT); see /reference/deployments for the live address.
Limit: a stale or wrong verifyingContract is the single most common cause of “every Path-B fill is rejected.” Why: the contract recomputes the same digest with its own deployed address; if your domain points at a dead core, the recovered signer is meaningless and on-chain verification fails for every quote. Mitigation: never hardcode the core address — load it from @inflexion/sdk (core.inflexionCore) or, in the engine, from the deployment registry. Use <AddressTable /> as the canonical source.
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.

The 13 fields, in order

Struct order = field order = signing order. EIP-712 hashes the struct field-by-field in declaration order, so this list is the canonical wire order an implementation in any language must reproduce (SignedQuoteTypes):
The TS-type split matters for serialization. Four fields are bigint — quotePrice, maxNotionalV0, validUntil, nonce (the uint128/uint64/uint256 fields). The rest (loadBps, every *Bps, model) stay plain number because they are uint16/uint8 and always < 2^53, so they are JSON-safe as-is. This is exactly the boundary the wire codec crosses.

CollateralModel

CollateralModel = { FULL: 0, PARTIAL: 1 }. FULL (0) is the only supported mode in v1 — set model: 0 and partialRatioBps: 0. PARTIAL is reserved for a future, quant-gated collateral model (see collateral models); every PARTIAL parameter is read from the calibrated risk-parameter set, never hardcoded.

The typehash pre-image

The EIP-712 typeHash is keccak256 of this exact ASCII string, with no spaces between fields and the field order above. It is exported as SIGNED_QUOTE_TYPE_STRING and is byte-identical to the contract’s SIGNED_QUOTE_TYPEHASH pre-image:
The full digest the contract computes (and that you must reproduce to verify off-chain) is the standard EIP-712 construction: typeHash=keccak256("SignedQuote(address mm,...,uint256 nonce)")\text{typeHash} = \mathrm{keccak256}\big(\texttt{"SignedQuote(address mm,...,uint256 nonce)"}\big) structHash=keccak256(typeHash ∥ mm ∥ marketId ∥ … ∥ nonce)\text{structHash} = \mathrm{keccak256}\big(\text{typeHash} \,\|\, \texttt{mm} \,\|\, \texttt{marketId} \,\|\, \dots \,\|\, \texttt{nonce}\big) domainSeparator=keccak256(keccak256("EIP712Domain(...)") ∥ keccak256("Inflexion") ∥ keccak256("1") ∥ chainId ∥ verifyingContract)\text{domainSeparator} = \mathrm{keccak256}\big(\mathrm{keccak256}(\texttt{"EIP712Domain(...)"}) \,\|\, \mathrm{keccak256}(\texttt{"Inflexion"}) \,\|\, \mathrm{keccak256}(\texttt{"1"}) \,\|\, \texttt{chainId} \,\|\, \texttt{verifyingContract}\big) digest=keccak256(0x1901 ∥ domainSeparator ∥ structHash)\text{digest} = \mathrm{keccak256}\big(\texttt{0x1901} \,\|\, \text{domainSeparator} \,\|\, \text{structHash}\big) Each value is ABI-encoded to 32 bytes: dynamic types (address, bytes32, the integers) are left-padded; there are no dynamic-length members, so there is no nested keccak256 of a string/bytes member. The exported helper quoteDigest(quote, chainId, verifyingContract): Hex returns exactly this digest via viem’s hashTypedData — use it as your cross-language conformance oracle.
Conformance check for a new language port. Hash a fixed test quote in your implementation and compare the digest byte-for-byte against quoteDigest(...). If the digests match, your domain, field order, and ABI encoding are all correct and the signature will verify on-chain. The definitive Stylus ≡ Solidity ≡ off-chain digest cross-check is itself a project integration test.

The wire codec

A bigint is not JSON-serializable, so the WS protocol, the POST /quote body, and the JSONL telemetry logs encode every uint128/uint64/uint256 field as a decimal string (QuoteWire). The uint16/uint8 fields stay as JS numbers.
The helpers are encodeQuote(q): QuoteWire (bigints → decimal strings) and decodeQuote(w): SignedQuote (strings → bigints).
Limit: signing the wire form (strings) instead of the typed form (bigints) silently produces a wrong digest. Why: EIP-712 hashes the integer fields as 32-byte numbers; a string member would hash as a dynamic string, changing the struct hash. Mitigation: always decodeQuote a wire payload back to bigints before re-signing or verifying it, and sign the typed object. The codec exists precisely so the transport layer and the signing layer never confuse their representations.

Signing the quote

There are two paths. The split is the same one that runs through the whole SDK: a key-based server path that builds a viem account from a raw private key, and a browser path that signs with the connected wallet and never touches a raw key.
The SDK method mm.signQuote(privateKey, quote, opts?) delegates to the engine signer (never reimplemented) and additionally enforces the two MM guards before returning:
  1. I10 — loadBps ≤ loadParams.maxLoadBps, else MmQuoteError('i10-exceeded').
  2. below-pool — loadBps < live pool-load-bps (from getPoolLoadToBeat), else MmQuoteError('not-below-pool'). If the pool load is unreadable the check is skipped (and reported) unless you pass requirePoolCheck: true (→ 'pool-load-unreadable').
mm-quote.ts
The raw engine helper underneath is signQuote(privateKey, quote, chainId, verifyingContract): Promise<QuoteEnvelope> — positional args, builds the account from the raw key. It is server-only; never use the raw-key path in the browser.

Verifying off-chain

verifyQuote(envelope, chainId, verifyingContract): Promise<boolean> returns true iff the signature recovers (case-insensitively) to quote.mm; recoverQuoteSigner(...) returns the EOA. This is the EOA path — EIP-1271 contract signers are verified on-chain only. The engine runs exactly this verify in its WS / POST /quote accept path before storing a quote.

What the chain re-checks at fill

Off-chain verification (and the engine’s freshness gate) is non-authoritative — the relayer only drops obvious junk to keep its store clean. The chain re-checks everything at createSwap / createSwapRouted, and a quote that fails any of these either reverts (forced Path B) or, under routing, falls back to Path A without reverting:
1

Signature & signer

The contract recomputes the digest with its own domain and recovers the signer; it must equal quote.mm (EOA via ecrecover, or EIP-1271 for a contract signer). A mismatched domain, field order, or wire-vs-typed signing surfaces here.
2

Validity window — I8/freshness

validUntil − block.timestamp ∈ [5, 15] s (VALIDITY_MIN_S = 5, VALIDITY_MAX_S = 15). This is the no-last-look design: firm GTD quotes with a tiny TTL that simply void themselves — the MM cannot pick off a stale price. The reference bot uses TTL = 10s, re-issuing every 5s, comfortably inside the window.
3

Price band — I9 / Fork 2

priceBandBps ∈ [25, 500] bps (0.25%–5%), and the fill reverts iff absBps(P_live, quotePrice) > priceBandBps. Crucially the band check reuses the same oracle read that pinned P0, so it is non-circular — see price pinning and the forks.
4

Capacity — I7

consumedNotional[quoteId] ≤ maxNotionalV0. The SDK previews this with capacityRemaining(quoteId, maxNotionalV0) → remaining = max(0, maxNotionalV0 − consumedNotional).
5

Nonce — I7

The bitmap bit (word, bit) decoded from nonce must be unspent. A cancelled bit cannot fill — the MM cancels by cancelNonces(...) on-chain, or simply by not refreshing the quote (the oracle band voids the stale one).
6

Load cap — I10

loadBps ≤ loadParams.maxLoadBps. On a Path-B fill the final premium is derived on-chain as premium=⌈FairPremium⋅(1+loadBps/10000)⌉\text{premium} = \lceil \text{FairPremium} \cdot (1 + \text{loadBps}/10000) \rceil, capped at MaxIL — the relayer returns a load, never a dollar premium.
Under createSwapRouted, an expired, over-band, under-collateralised, or otherwise invalid MM quote does not revert the LP’s transaction — the router falls back to the always-on cvAMM pool, so the LP always clears at the cheaper of {pool, valid MM quote}. Only the executed rail mutates nonce / capacity / lock. See routing.

Why the quote carries no tokenId

MaxIL is both the load-bearing payout cap and the unit of risk: pure geometry, frozen at creation, L-independent in the fair-rate sense — which makes positions fungible to an underwriter within a market (see MaxIL). So a SignedQuote prices a market, not a position: a single loadBps over the on-chain FairPremium, a [minMaxILRatioBps, maxMaxILRatioBps] band, and maxNotionalV0 capacity. The geometry-independent “load to beat” is itself a first-class streamable signal — getPoolLoadToBeat / streamPoolLoadToBeat give the pure pool floor an MM must undercut with no position geometry at all. This is also why fill attribution is per-quote (quoteId / nonce), not per-NFT — the precise QuoteFilled event carries (swapId, mm, quoteId, nonce, loadBps).
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.

Field cheat-sheet for a fresh quote

A minimal, valid FULL-mode quote for the live deployment:
encodeNonce(word, bit) = (word << 8) | bit with bit ∈ [0, 255]; decodeNonce(nonce) = { word: nonce >> 8, bit: nonce & 0xff }; wadToBps(wad) = floor(wad · 10000 / 1e18) — all exported from @inflexion/sdk.

Engine — the Path-B relayer

Where you publish the signed envelope: the WS protocol, POST /quote, freshness gate, and the day-one telemetry sinks.

MM client

signQuote, getPoolLoadToBeat, quoteStream, capacityRemaining, and the I10 + below-pool guards in code.

marketId parity

How marketId is derived from (token0, token1, fee, durationSeconds) and stays consistent across chain, SDK, and engine.

The forks (I8 / I9)

Why the validity window and the oracle-anchored price band make Path B a no-last-look design.