State Machine
Every trade moves through six states:Transport at a Glance
Step-by-Step Flow
1
Open a quote request
The frontend creates a live quote request by calling the backend. No wallet signature is required — the quote request is unsigned.The backend broadcasts the trade parameters to all subscribed market makers as an SSE
On a buy the taker submits a USDC budget (
trade.budgetUsd); makers quote a per-option price and the taker receives floor(budgetUsd / price) whole options. On a sell the taker submits a contract count (trade.size).quote_request event and returns a requestId and expiresAt timestamp. Quote requests are long-lived (5 minutes by default, configurable via QUOTE_REQUEST_TTL_MS).2
Market makers submit live quotes (quoted)
Makers subscribed to the quote-request stream receive the The endpoint returns a server-generated
quote_request event and submit quotes over REST:quoteId. Makers can update their quotes at any time by re-POSTing for the same requestId — only the latest quote per maker is kept. The side echoes the taker’s requested side; the price is the ask (for taker buys) or bid (for taker sells).3
Frontend streams live pricing
While the quote request is open, the frontend subscribes to a user-facing SSE stream to display the live best quote as it changes:The stream emits
best_quote events (with the current best quote, maker, and quote count) and a closed event when the request ends. This replaces the old ~1s polling of GET /quote-requests/:id/quotes, which still exists as a fallback.4
Trader commits (accepted)
When the trader is ready, they commit by providing their wallet address:The backend selects the best valid quote, resolves the
seriesId from the option parameters, builds the exact EIP-712 Order struct (with maker set to the winning quote’s wallet and taker set to the committing wallet), and emits a WebSocket quote:accepted message to the winning maker, starting a confirmation deadline. The backend does not post collateral, approve, or sign on the maker’s behalf — makers are independent and manage their own USDC approvals.5
Maker confirms (confirmed)
The winning maker signs the The backend verifies the signature recovers to
Order with its own wallet key and POSTs the signature to the confirm endpoint before the confirmationDeadline (MAKER_CONFIRMATION_MS, default 10s):order.maker, returns the signed order to the trader, and emits WS quote:confirmed to the winner and quote:rejected to the other makers.Fallback: if the winning maker misses the deadline, the backend falls back to the next-best maker’s quote and emits quote:accepted to them. If no maker confirms, the request stays open and the taker can retry — the commit returns HTTP 503.6
Taker calls fill() on-chain (executed)
The taker submits the maker’s signed order to
ConvallaxRFQSettlement.fill(), which calls ConvallaxCore.mintFor(...) to execute everything in one transaction — atomic mint-on-fill. Option tokens are minted fresh to the holder (never transferred from inventory), and the writer’s USDC collateral is locked at the same time:- Taker buying (long,
makerSelling = true): the maker posts collateral to the Core vault (maker becomes the writer), option tokens are minted to the taker (the holder), and premium flows taker → maker. - Taker selling (short,
makerSelling = false): the taker posts collateral to the Core vault (taker becomes the writer), option tokens are minted to the maker (the holder), and premium flows maker → taker.
7
Settlement at expiry (settled)
At expiry, a resolution price is computed off-chain and submitted on-chain via a signed attestation. Holders and writers then claim their respective payouts. This flow is unchanged — see Settlement.
Deadlines
- Maker misses deadline within
expires_at: the request stays open (the missed maker’s quote is withdrawn) and the backend falls back to the next-best maker. - Past
expires_at: the request is expired and aquote_request_expiredSSE event is emitted to makers.
Winner Selection
The backend selects the best valid quote at commit time:
Validation rules:
sidemust match the taker’s requested side (echoestrade.side)priceis per one option, in(0, 1), and within no-arbitrage bounds- On a buy,
sizeis the max whole options you’ll fill (≥ 1); the actual fill ismin(floor(budgetUsd / price), size) - On a sell,
sizemust be ≥ 50% of the requestedtrade.size
On-Chain Order (EIP-712)
TheOrder struct signed by the market maker for on-chain settlement:
Two Signature Domains
Convallax uses two distinct EIP-712 domains:Unlike the previous architecture, traders no longer sign an EIP-712 message to initiate a trade. The trader’s authorization is the on-chain
fill() call itself — only the wallet that is designated as the taker in the signed order can execute the fill.Premium Calculation
premiumAmount= 0.12 × 10 × 1,000,000 = 1,200,000 raw (1.20 USDC)optionAmount= 10 × 1,000,000 = 10,000,000 raw
