Skip to main content
Convallax splits maker transport into three channels. The WebSocket is channel 3 — the post-trade channel. It is a server → maker push connection that delivers a single thing: notification that one of your quotes has won (or lost) once a taker commits. It no longer carries quote requests or quote submissions.
ChannelTransportDirectionPurpose
1 — Quote requestsSSEserver → makerReceive live quote_request events (Quote Request Stream)
2 — Quote submissionRESTmaker → serverPOST /v1/mm/quotes and confirm signature
3 — Post-tradeWebSocketserver → makerquote:accepted / quote:confirmed / quote:rejected (this page)
A maker must be connected to both the SSE quote-request stream (to receive requests) and this WebSocket (to receive quote:accepted) to fully participate. Confirmation signatures go back over REST (channel 2), not over the WebSocket.
Convallax remains non-custodial: makers sign their own EIP-712 settlement orders with their own wallet key. This transport split changes only how messages move, not the trust model.

Connecting

wss://api.convallax.com/maker/v1/ws

Authentication

Each market maker authenticates with a per-MM API key generated from the dashboard. The key maps to a stable makerId. There are two equivalent ways to authenticate: Option A — query parameter:
wss://api.convallax.com/maker/v1/ws?apiKey=<your-key>
Option B — auth message sent immediately after the socket opens:
{
  "type": "auth",
  "apiKey": "<your-key>"
}
On successful authentication, the server sends a confirmation that echoes your resolved makerId:
{
  "type": "connected",
  "protocolVersion": 3,
  "makerId": "mm-alpha",
  "authenticated": true,
  "serverTime": "2026-06-15T12:00:00.000Z"
}
If the API key is missing or invalid (when the relay is configured with keys), the connection is closed with code 4001.
If the relay is started without any maker keys configured, it runs in OPEN dev mode — it accepts anyone and assigns an anonymous makerId. See the Authentication guide for full details and how to obtain a key.
The post-trade WebSocket still authenticates via ?apiKey= or the auth message. The SSE quote-request stream and the REST quote endpoints authenticate via the X-API-Key header instead (see Authentication).

Keep-Alive

The server sends a WebSocket ping every ~25 seconds; your client must respond with a pong to maintain the connection (most WebSocket libraries handle this automatically). You can also send an application-level keep-alive at any time:
{ "type": "ping" }
The relay replies with:
{ "type": "pong", "timestamp": "2026-06-15T12:00:00.000Z" }

Post-Trade Events

The WebSocket delivers only post-trade events. There is no quote_request, quote, order_to_sign, or order_signed on this channel anymore — those moved to SSE (in) and REST (out).

quote:accepted — You won; sign and confirm

Sent only to the winning maker when a taker commits. It carries the exact Order struct, the EIP-712 domain and types, and a confirmationDeadline. Sign the order with the wallet key for order.maker, then POST the signature to POST /v1/mm/quotes/:quoteId/confirm before confirmationDeadline.
{
  "type": "quote:accepted",
  "quoteId": "8f3c2b1a-...",
  "requestId": "req-789",
  "order": {
    "maker": "0xYourMakerWallet...",
    "seriesId": "98765432109876543210",
    "optionAmount": "10000000",
    "premiumAmount": "1200000",
    "makerSelling": true,
    "taker": "0xTrader...",
    "validUntil": 1717189320,
    "nonce": 1717189200042
  },
  "domain": {
    "name": "ConvallaxRFQSettlement",
    "version": "1",
    "chainId": 80002,
    "verifyingContract": "0x721C428fb5a5468698C295dD4DC2D7bE06479f21"
  },
  "types": {
    "Order": [
      { "name": "maker", "type": "address" },
      { "name": "seriesId", "type": "uint256" },
      { "name": "optionAmount", "type": "uint256" },
      { "name": "premiumAmount", "type": "uint256" },
      { "name": "makerSelling", "type": "bool" },
      { "name": "taker", "type": "address" },
      { "name": "validUntil", "type": "uint256" },
      { "name": "nonce", "type": "uint256" }
    ]
  },
  "confirmationDeadline": "2026-06-15T12:00:11.000Z"
}
The order / domain / types shapes are identical to the legacy order_to_sign message — only the transport changed. The backend verifies that your signature recovers to order.maker.
If you miss the confirmationDeadline, the backend falls back to the next-best maker’s quote. Set your USDC approvals (Core for collateral, Settlement for premium) before quoting so you can sign instantly — collateral is locked only at fill, so there is no inventory to pre-position.

quote:confirmed — Your quote won and was confirmed

Sent after your confirmation signature is verified. Your trade is locked in and the taker can now settle on-chain.
{
  "type": "quote:confirmed",
  "requestId": "req-789",
  "quoteId": "8f3c2b1a-..."
}

quote:rejected — Another maker won (or the request closed)

Sent when another maker’s quote won, or when the request closed without your quote winning. Drop the request from local state.
{
  "type": "quote:rejected",
  "requestId": "req-789",
  "quoteId": "8f3c2b1a-...",
  "reason": "another_quote_won"
}

Protocol Version

The current protocol version is 3. The protocolVersion field in the connected message tells you which version the relay is running. Check the version on connect to ensure compatibility.
Migrating from protocol v2: The WebSocket no longer carries quote_request, quote, order_to_sign, or order_signed. Quote requests now arrive over the SSE quote-request stream; quotes and confirmation signatures are submitted over REST. The WebSocket is now post-trade only, delivering quote:accepted, quote:confirmed, and quote:rejected.

Example: Full Maker Lifecycle Across All Three Channels

This example subscribes to the SSE quote-request stream for incoming requests, POSTs quotes via REST, listens on the WebSocket for quote:accepted, signs with ethers, and POSTs the signature to the confirm endpoint.
import EventSource from 'eventsource';
import WebSocket from 'ws';
import { Wallet } from 'ethers';

const API = 'https://api.convallax.com';
const WS = 'wss://api.convallax.com';
const API_KEY = process.env.MAKER_API_KEY;
const wallet = new Wallet(process.env.MM_PRIVATE_KEY);

// quoteId is server-generated; remember it per request so we can sign on accept.
const quoteIds = new Map(); // requestId -> quoteId

// --- Channel 1: SSE quote-request stream (server -> maker) ---
const stream = new EventSource(`${API}/v1/mm/quote-requests/stream`, {
  headers: { 'X-API-Key': API_KEY },
});

stream.addEventListener('connected', (e) =>
  console.log('SSE connected', JSON.parse(e.data))
);

stream.addEventListener('quote_request', async (e) => {
  const req = JSON.parse(e.data);
  const { option, trade } = req.params;
  const price = computePrice(option, trade); // your pricing model

  // --- Channel 2: submit the quote over REST ---
  const res = await fetch(`${API}/v1/mm/quotes`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
    body: JSON.stringify({
      requestId: req.requestId,
      quote: {
        maker: wallet.address, // MUST be your own wallet
        side: trade.side,      // must match the taker's side
        price,
        size: trade.size,
      },
    }),
  });
  const { quoteId } = await res.json();
  quoteIds.set(req.requestId, quoteId); // server-generated, stable across updates
});

stream.addEventListener('quote_request_expired', (e) => {
  const { requestId } = JSON.parse(e.data);
  quoteIds.delete(requestId);
});

// --- Channel 3: post-trade WebSocket (server -> maker) ---
const ws = new WebSocket(`${WS}/maker/v1/ws?apiKey=${API_KEY}`);
ws.on('ping', () => ws.pong());

ws.on('message', async (raw) => {
  const msg = JSON.parse(raw);

  switch (msg.type) {
    case 'connected':
      console.log(`WS v${msg.protocolVersion} as ${msg.makerId}`);
      break;

    case 'quote:accepted': {
      // You won — sign the order and confirm over REST before the deadline.
      const signature = await wallet.signTypedData(msg.domain, msg.types, msg.order);
      await fetch(`${API}/v1/mm/quotes/${msg.quoteId}/confirm`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
        body: JSON.stringify({ signature }),
      });
      break;
    }

    case 'quote:confirmed':
      console.log(`Confirmed ${msg.quoteId} for ${msg.requestId}`);
      break;

    case 'quote:rejected':
      console.log(`Rejected ${msg.quoteId}: ${msg.reason}`);
      break;
  }
});