Skip to main content
All trading on Convallax flows through a live quote request system. Traders open a quote request, independent market makers compete in real time, the trader commits to the best quote, the winning maker signs its own settlement order, and the taker fills it atomically on-chain. Maker transport is split across three channels: quote requests are pushed to makers over SSE, quotes and confirmation signatures are submitted over REST, and post-trade win/loss notifications are pushed over a WebSocket. The state machine is unchanged — only the transport that drives each transition differs.

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.
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).
The backend broadcasts the trade parameters to all subscribed market makers as an SSE 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 quote_request event and submit quotes over REST:
The endpoint returns a server-generated 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 Order with its own wallet key and POSTs the signature to the confirm endpoint before the confirmationDeadline (MAKER_CONFIRMATION_MS, default 10s):
The backend verifies the signature recovers to 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.
The fill is atomic — collateral, premium, and minting all execute in a single transaction or the entire trade reverts. Collateral is locked only when a trade actually executes; there is no pre-minting or inventory. The nonce is consumed to prevent replay.
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 a quote_request_expired SSE event is emitted to makers.

Winner Selection

The backend selects the best valid quote at commit time: Validation rules:
  • side must match the taker’s requested side (echoes trade.side)
  • price is per one option, in (0, 1), and within no-arbitrage bounds
  • On a buy, size is the max whole options you’ll fill (≥ 1); the actual fill is min(floor(budgetUsd / price), size)
  • On a sell, size must be ≥ 50% of the requested trade.size

On-Chain Order (EIP-712)

The Order struct signed by the market maker for on-chain settlement:
EIP-712 domain:

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

For example, buying 10 options at price $0.12:
  • premiumAmount = 0.12 × 10 × 1,000,000 = 1,200,000 raw (1.20 USDC)
  • optionAmount = 10 × 1,000,000 = 10,000,000 raw