@inflexion/engine is the off-chain service that powers the optional
Path-B (market-maker) rail. It is deliberately thin: a
framework-free node:http process whose entire job is to relay firm, signed MM quotes
to LPs and to capture the two off-chain halves of the data moat.
It is pure upside — if no MM is ever connected, the cvAMM pool (Path A)
still clears every position with no relayer in the loop.
Streams firm quotes
MMs push EIP-712 signed quotes over WebSocket; the relayer verifies, freshness-gates,
and stores the latest per
(market, MM).Serves the price-to-beat
GET /quote returns the cheapest currently-valid quote (lowest loadBps) — a
load, never a dollar premium.Captures day-one signals
Latent demand (Signal 4) and quote competition (Signal 2) are logged to append-only
JSONL — they are unreconstructable retroactively.
What it is — and what it deliberately is NOT
The engine surrounds the on-chain core; it is never in the trust path. The chain is the authority for matching, pricing, and settlement. The three things it does do:1
Accept and verify firm quotes
Take EIP-712 signed quotes streamed by an MM over WebSocket, verify the signature
plus a light freshness gate, and store the latest per
(market, MM).2
Serve the cheapest live quote
Answer
GET /quote?marketId=… with the lowest-loadBps quote that is not expired —
the price-to-beat the LP sees before going on-chain.3
Capture the two day-one moat signals
Append latent demand (Signal 4) and quote competition (Signal 2) to JSONL sinks that
are otherwise impossible to reconstruct after the fact.
Why Path A needs no engine at all. Path A — the cvAMM pool —
is always-on and signature-free; a pooled vault underwrites with no relayer involved.
The engine exists only so a sophisticated MM can compete on
price against the pool.
createSwapRouted then gives the LP the
cheaper of {pool, MM quote}, and falls back to the pool whenever no MM is connected.Configuration — environment variables
The process reads its config from env and starts the relayer:VERIFYING_CONTRACT and CHAIN_ID default from the deployment registry, never
hardcoded: the engine reads the on-chain address registry directly. The engine cannot import
@inflexion/sdk — the SDK depends on the engine, which would be a cycle.
Startup logs the four surfaces; SIGINT/SIGTERM trigger a graceful
close() that shuts the WS server, then the HTTP server.
The on-the-wire unit — SignedQuote
The SignedQuote module is the single source of truth for how a Path-B MM
signs a quote and how anyone verifies it off-chain. The field order, types, and the
domain ("Inflexion", "1") must match InflexionCore.SignedQuote /
SIGNED_QUOTE_TYPEHASH exactly, or on-chain verification in
createSwap/createSwapRouted rejects the signature. Both the SDK and
the MM bot import this module.
The EIP-712 domain is { name: 'Inflexion', version: '1', chainId, verifyingContract }.
The 13 quote fields, in struct/field/signing order:
The canonical type-string is mirrored in
SIGNED_QUOTE_TYPE_STRING
and is byte-identical to InflexionCore.SIGNED_QUOTE_TYPEHASH’s pre-image.
The quote is PER-MARKET, not per-NFT — and that is load-bearing. MaxIL
is pure geometry, frozen at creation, and L-independent in the fair-rate sense, which
makes positions fungible to an underwriter within a market. So an MM quote is a
load + a MaxIL-ratio band + capacity —
loadBps + [minMaxILRatioBps, maxMaxILRatioBps]maxNotionalV0— never tied to one position. TheSignedQuotecarries notokenId.
Signing and verifying
The four helpers:Hex
The EIP-712 digest
_hashTypedDataV4(hashQuote(q)) that the contract computes.{ quote, signature }
Produces the signed envelope.
Address
Recovers the EOA signer from a
{ quote, signature } envelope.boolean
True iff the signature recovers to
quote.mm, case-insensitively.
This is the EOA path; EIP-1271 contract signers are verified on-chain only.Wire codec.
bigint is not JSON-serializable, so the WS
protocol and the JSONL logs encode every uint128/uint64/uint256 field as a decimal
string. encodeQuote(q) produces the QuoteWire (bigints → strings); decodeQuote(w)
parses them back. The small uint16/uint8 fields stay JS number (always < 2^53,
safe).On-chain bounds the off-chain quote must respect
The relayer applies only a light freshness gate; the chain re-checks everything. The authoritative bounds (fromInflexionCore) a quote must satisfy or
createSwapRouted reverts / falls back to Path A:
Validity window — validUntil − now ∈ [5, 15] s
Validity window — validUntil − now ∈ [5, 15] s
VALIDITY_MIN_S = 5, VALIDITY_MAX_S = 15, with validUntil bounded on-chain to
now + [5, 15] s. The MM bot uses TTL = 10s and re-issues
every 5s, comfortably inside the window.Why so short: firm GTD quotes with a tiny TTL plus an oracle-anchored band are the
no-last-look design — the MM cannot pick off stale prices, and a stale quote
simply voids itself.Price band — priceBandBps ∈ [25, 500] bps
Price band — priceBandBps ∈ [25, 500] bps
PRICE_BAND_MIN_BPS = 25 (0.25%), PRICE_BAND_MAX_BPS = 500 (5%).
At createSwap the contract reverts iff
(invariant I9 / Fork 2). The band check
reuses the same oracle read that pinned P0, so it is non-circular.Routing fallback — never revert on a bad quote
Routing fallback — never revert on a bad quote
During routing, an expired / over-band / under-collateralised
quote causes
_tryQuote-style logic to fall back to Path A, never revert.
Only the executed rail mutates nonce/capacity/lock (I7).The WebSocket protocol (MM → relayer)
The WS server is attached to the same HTTP server (new WebSocketServer({ server: http })). The MM streams messages; the relayer replies
per message. Inbound shape:
1
Parse
Non-JSON →
{ "type": "error", "error": "invalid json" }.2
Shape check
Missing
type:'quote' / quote / signature →
{ "type": "error", "error": "expected {type:'quote', quote, signature}" }.3
Address check
quote.mm not a valid address → { "type": "error", "error": "bad mm address" }.4
Freshness gate
freshnessOk rejects if validUntil ≤ now (already expired)
or validUntil > now + maxValiditySkewS (dated too far out; default
maxValiditySkewS = 60s). On failure: logs the quote as a loser
and replies { "type": "rejected", "reason": "stale-or-far-validUntil" }.5
Signature verify
verifyQuote (EIP-712 recover). On failure: logs as loser, replies
{ "type": "rejected", "reason": "bad-signature" }.6
Accept
Logs as winner/candidate, stores the latest per
(market, MM), replies
{ "type": "ack", "marketId": "0x...", "loadBps": 500 }.ack (accepted), rejected (freshness or signature failure, with a
reason), error (malformed message). Logging is best-effort and never blocks the
ack.
The freshness gate is intentionally non-authoritative — “the chain
re-checks everything.” The relayer’s
maxValiditySkewS = 60s is looser than the
on-chain [5, 15]s window; that is deliberate. The relayer’s job is only to drop obvious
junk and keep the store clean, while the chain is the real gate (see
on-chain bounds).HTTP endpoints (relayer)
CORS is open on every route (Access-Control-Allow-Origin: *,
Allow-Methods: GET, POST, OPTIONS, Allow-Headers: content-type); preflight OPTIONS
returns 204. Why CORS-open: the browser dApp is a different
origin and must both read GET /quote (the SDK’s previewPremium) and publish
POST /quote (browser-side MM signing).
GET /health
Sink + market status:
markets = number of markets currently holding ≥1 quote (store.marketCount());
telemetry.* reflects whether each sink path is configured.
GET /quote?marketId=0x… — the cheapest live quote
The taker-side read:
marketIdmust be a bytes32 hex (/^0x[0-9a-fA-F]{64}$/); else400{ "error": "marketId (bytes32 hex) required" }.- Logs the request as latent demand before answering (Signal 4, best-effort, never blocks).
- Looks up
store.best(marketId, now)= the cheapest currently-valid quote (lowestloadBps, not expired). - No live quote →
404{ "error": "no live quote for market", "marketId": "0x…" }. - Hit →
200:
note is the canonical reminder that the relayer returns a
load, not a dollar premium.
POST /quote — browser-friendly publish
Same verify + freshness + store as the WS path, for browser MMs that cannot hold a
persistent socket. Body is read up to 8192 bytes:
The HTTP path uses
422 for verify/freshness rejections versus the WS path’s
{ type:'rejected' } message; both record the same telemetry reason.POST /telemetry/preview — the Signal-4 demand ping
The SDK’s previewPremium best-effort POST. Body is read up to 4096 bytes
and carries only bucketed geometry + marketId — no PII:
- valid marketId →
202 { "ok": true, "logged": true }—logged:falsewhen no sink is configured. - missing/invalid marketId →
400 { "ok": false, "reason": "marketId (bytes32 hex) required" }.
logged:false, never an exception.
Unmatched routes → 404 { "error": "not found" }.
The day-one telemetry sinks
The core WHY — the data moat. On-chain we only ever observe realized fills. Everything an LP priced but did not buy (Signal 4 — latent demand) and every MM quote that lost or was withdrawn (Signal 2 — the dynamic quote-competition half) leaves no on-chain trace by design — invariant I7: an unchosen quote touches no nonce/capacity. If it is not logged now, it is gone forever. These two signals must be captured from the very first interaction, long before any/data endpoint, subgraph, or API exists to serve them.
Privacy-preserving geometry buckets — the SDK computes these
client-side and POSTs only the labels; the logs hold no raw geometry, no tokenId, no
addresses:
Unknown/invalid input coerces to
'unknown' (asWidthBucket etc.).
Demand log (Signal 4) — DEMAND_LOG → DemandRecord
Written on every GET /quote request and every POST /telemetry/preview ping.
filled is always false at write time — this log is
exactly the demand the chain never sees. Realized fills are
reconstructed later from on-chain SwapCreated/SwapPriced.
logQuoteRequest(marketId, now)— emitted byGET /quote; buckets are'unknown'(a bare lookup carries no geometry),source: 'quote-request'.ingestPreview(input, now)— emitted byPOST /telemetry/preview; validates and normalizes the loosely-typed body,source: 'preview'.normalizeMarketIdrequires bytes32 hex;normalizePremiumcoerces a string/number/bigint into a bigint-safe decimal string or drops it. Returns{ ok, logged, reason? }.
Competition log (Signal 2) — COMPETITION_LOG → CompetitionRecord
Written for every inbound WS quote — winners AND losers (logQuote),
called from both the WS handler and POST /quote.
Why log losers too. Only ever logging the latest stored quote (as the quote store does)
loses the competition — who else quoted, how wide, who widened or withdrew under stress.
The chain only sees the realized fill, so the full competing field
is captured only here.
accepted marks whether the relayer stored it as a candidate
(signature + freshness valid); a rejected quote still competed and is still logged.get demandEnabled / get competitionEnabled report whether
each sink path is configured, and are surfaced in /health.
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.
The quote store
An in-memory mapmarketId(lc) → mm(lc) → { env, receivedAt } — the
latest quote per (market, MM), with an optional append-only JSONL log (QUOTE_LOG):
put(env, now)— store/replace the latest for its(market, MM); iflogPathis set, append{ t, quote, signature }as JSONL.best(marketId, nowSec)— the cheapest currently-valid quote = lowestloadBpsamong quotes whosevalidUntil > now. This is the price-to-beat the LP sees; the on-chain router still re-derives everything.marketCount()— number of markets with ≥1 quote.
The example MM bot
The example MM bot is a reference bot that signs a freshSignedQuote on a
loop and streams it over WS, demonstrating the firm-quote / short-TTL model: each quote is
GTD (validUntil = now + ttl) and simply re-issued; the MM “cancels” by not refreshing
— and the on-chain oracle band voids any stale quote.
TTL_S = 10,REISSUE_MS = 5000— re-issue every 5s, each valid 10s, always inside the on-chain[5, 15]swindow.- Env:
MM_PRIVATE_KEY(required),MARKET_ID(required),ENGINE_WS(defaultws://localhost:8787),CHAIN_ID(421614),VERIFYING_CONTRACT,QUOTE_PRICE,LOAD_BPS(default500= 5%). - Default quote:
minMaxILRatioBps=0,maxMaxILRatioBps=10000(accept any MaxIL ratio),priceBandBps=100(1%, inside[25,500]),model=FULL,maxNotionalV0 = 2^128−1(unbounded capacity), monotonically incrementingnonce.
Worked examples — curl + wscat
How the engine fits the end-to-end flow
The relayer is step 1 (and part of step 2) of the create→settle lifecycle:1
MM streams a firm quote
The MM pushes
{type:'quote', quote, signature} over WS. The relayer verifies
EIP-712 + freshness, stores it as the cheapest-per-market candidate, and logs it to
COMPETITION_LOG as a winner.2
LP previews
The SDK’s
previewPremium reads the live pool-load surface (via RPC or the API
/pool/load-surface) and GET /quote (the cheapest MM load), then best-effort POSTs
/telemetry/preview (logged to DEMAND_LOG).3
LP buys on-chain
The LP calls
createSwapRouted on InflexionCore. The chain
re-derives FairPremium (the Φ-sum from the Stylus
FairValueOracle), re-checks the validity window [5,15]s, the price band (I9), the
nonce (I7), and routes to the cheaper of {cvAMM pool, MM quote}; an invalid quote
falls back to Path A, never reverts.4
Settlement
The protocol pays the LP
min(realized_IL, MaxIL) trustlessly from the chosen
underwriter’s collateral. The relayer is never in this path.The two separate claims
The engine relays quotes and captures demand; the on-chain core enforces the guarantees. Routing through the engine changes who underwrites and at what price, never whether the LP is paid: And the capital-at-risk disclosure applies to whoever ends up on the other side — the pool’s depositors on Path A, or the market maker on Path B: Network: Arbitrum Sepolia · chainId421614 · 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
Path B — the MM rail
Firm EIP-712 quotes, no last-look, and the three on-chain protections.
createSwapRouted
How the chain prices both rails off one FairPremium and never reverts on a bad quote.
Quote signing
The EIP-712 type-string, digest, and the Stylus≡Solidity≡off-chain parity check.
The MM client (SDK)
The typed client that wraps quote signing + WS streaming for you.
The public REST API
The read-only facade that reads the engine’s telemetry sinks for the moat surfaces.
The data moat — five signals
Where latent demand (4) and quote competition (2) land in the full taxonomy.