Skip to Content
HyperQuote is live on HyperEVM — Start trading →
API ReferenceRelay WebSocket Protocol

Relay WebSocket Protocol

The HyperQuote relay is a WebSocket server that handles real-time RFQ broadcasting and quote collection. It validates message structure, verifies EIP-712 signatures on quotes, enforces rate limits, and broadcasts messages to all connected clients.

Connection

WebSocket URL

Production: wss://relay.hyperquote.xyz Local: ws://127.0.0.1:8080

Connect using any WebSocket client. No authentication handshake is required — all connected clients immediately receive RFQ broadcasts and can submit quotes.

import WebSocket from "ws"; const ws = new WebSocket("wss://relay.hyperquote.xyz"); ws.on("open", () => { console.log("Connected to relay"); }); ws.on("message", (raw) => { const msg = JSON.parse(raw.toString()); handleMessage(msg); }); ws.on("close", () => { console.log("Disconnected from relay"); });

Message Format

All messages follow the same JSON envelope:

interface RelayMessage { type: "RFQ_BROADCAST" | "QUOTE_SUBMIT" | "QUOTE_BROADCAST" | "CANCEL_REQUEST" | "PING" | "PONG" | "ERROR"; data: unknown; }

Message Types

Client → Relay

TypeDescription
QUOTE_SUBMITSubmit a maker’s signed EIP-712 quote for an active RFQ
CANCEL_REQUESTCancel an active RFQ (taker only)
PINGKeepalive ping
PONGResponse to relay’s PING

Relay → Client

TypeDescription
RFQ_BROADCASTA new RFQ has been created and is available for quoting
QUOTE_BROADCASTA validated quote has been submitted for an active RFQ
PONGResponse to client’s PING
PINGKeepalive check from relay
ERRORValidation or processing error

RFQ_BROADCAST

Sent to all connected clients when a new RFQ is created and validated. Makers use this to decide whether to submit a quote.

{ "type": "RFQ_BROADCAST", "data": { "rfqId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "rfq": { "taker": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "tokenIn": "0xb88339cb7199b77e23db6e890353e22632ba630f", "tokenOut": "0x5555555555555555555555555555555555555555", "amountIn": "1000000000", "amountOut": null, "kind": 0, "expiry": 1710086430, "visibility": "public" } } }

RFQ Fields

FieldTypeDescription
takeraddressWallet that created the RFQ
tokenInaddressToken the taker is selling
tokenOutaddressToken the taker wants to receive
amountInstringInput amount in raw token units (for EXACT_IN)
amountOutstring | nullOutput amount in raw token units (for EXACT_OUT), or null
kindnumber0 = EXACT_IN, 1 = EXACT_OUT
expirynumberUnix timestamp when the RFQ expires
visibilitystring"public" or "private"

QUOTE_SUBMIT

Submit a signed EIP-712 quote in response to an active RFQ. The relay verifies the signature, validates the quote parameters against the RFQ, and broadcasts the quote to all clients.

{ "type": "QUOTE_SUBMIT", "data": { "rfqId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "quote": { "maker": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "taker": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "tokenIn": "0xb88339cb7199b77e23db6e890353e22632ba630f", "tokenOut": "0x5555555555555555555555555555555555555555", "amountIn": "1000000000", "amountOut": "50000000000000000000", "expiry": 1710086460, "nonce": "42", "deadline": 1710086460 }, "signature": "0x..." } }

Quote Fields

FieldTypeDescription
makeraddressMaker wallet address (signer)
takeraddressTaker wallet address (from the RFQ), or 0x0 for open quotes
tokenInaddressInput token address (must match RFQ)
tokenOutaddressOutput token address (must match RFQ)
amountInstringInput amount (BigInt string)
amountOutstringOutput amount offered by the maker (BigInt string)
expirynumberQuote expiry timestamp (Unix seconds)
noncestringMaker nonce for replay protection
deadlinenumberLatest time this quote can be filled on-chain

Quote Validation

The relay validates every quote before broadcasting:

RuleConstraint
RFQ activeReferenced rfqId must be active and unexpired
Token matchingtokenIn and tokenOut must match the RFQ
Deadline in futuredeadline > now
Non-zero amountsamountIn > 0 and amountOut > 0
EIP-712 signatureRecovered address must match quote.maker
UniquenessOne quote per maker per RFQ

EIP-712 Signing

Quote signatures must use the SpotRFQ contract’s EIP-712 domain:

const domain = { name: "HyperQuote", version: "1", chainId: 999, // HyperEVM verifyingContract: SPOT_RFQ_ADDRESS, }; const QUOTE_TYPES = { Quote: [ { name: "maker", type: "address" }, { name: "taker", type: "address" }, { name: "tokenIn", type: "address" }, { name: "tokenOut", type: "address" }, { name: "amountIn", type: "uint256" }, { name: "amountOut",type: "uint256" }, { name: "expiry", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, ], }; // Sign the quote const signature = await signer.signTypedData(domain, QUOTE_TYPES, quoteValues);

Field order matters. The fields must appear in exactly the order shown above, matching the Solidity QUOTE_TYPEHASH. Reordering fields produces a different typehash and all signatures will fail verification.

QUOTE_BROADCAST

Sent to all connected clients when a quote passes validation:

{ "type": "QUOTE_BROADCAST", "data": { "rfqId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "quote": { "maker": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "taker": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", "tokenIn": "0xb88339cb...", "tokenOut": "0x55555555...", "amountIn": "1000000000", "amountOut": "50000000000000000000", "expiry": 1710086460, "nonce": "42", "deadline": 1710086460 }, "signature": "0x..." } }

CANCEL_REQUEST

Sent by the taker to cancel an active RFQ. The relay removes the RFQ from the active store and stops broadcasting quotes for it.

{ "type": "CANCEL_REQUEST", "data": { "rfqId": "f47ac10b-58cc-4372-a567-0e02b2c3d479" } }

If the cancellation succeeds, no response is sent. The RFQ simply stops appearing in /rfqs listings and new quotes for it are rejected. If the RFQ is not found or already expired, the relay sends an ERROR message.

ERROR

Sent to the submitting client when validation fails:

{ "type": "ERROR", "data": { "message": "RFQ not found or expired" } }

See Error Codes for the full list of relay error messages.

Heartbeat / Ping-Pong

Send a PING message every 30 seconds to keep the connection alive. The relay responds with PONG:

// Send keepalive PING every 30 seconds const pingInterval = setInterval(() => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: "PING", data: {} })); } }, 30_000); // Respond to relay PING with PONG ws.on("message", (raw) => { const msg = JSON.parse(raw.toString()); if (msg.type === "PING") { ws.send(JSON.stringify({ type: "PONG", data: {} })); } }); // Clean up on disconnect ws.on("close", () => clearInterval(pingInterval));

Reconnection Strategy

Implement auto-reconnect with exponential backoff to handle network drops and relay restarts:

const MAX_RECONNECT_ATTEMPTS = 10; const BASE_DELAY_MS = 1000; let attempt = 0; function connect() { const ws = new WebSocket("wss://relay.hyperquote.xyz"); ws.on("open", () => { console.log("Connected to relay"); attempt = 0; // Reset on success }); ws.on("close", () => { if (attempt >= MAX_RECONNECT_ATTEMPTS) { console.error("Max reconnect attempts reached."); process.exit(1); } const delay = BASE_DELAY_MS * Math.pow(2, attempt); attempt++; console.log(`Reconnecting in ${delay}ms (attempt ${attempt})...`); setTimeout(connect, delay); }); ws.on("error", (err) => { console.error("WebSocket error:", err.message); }); // ... message handlers } connect();

REST Endpoints

The relay also exposes REST endpoints on the same port for polling:

MethodEndpointDescription
GET/rfqsList active, unexpired RFQs with quote counts
GET/quotes?rfqId=<id>List quotes for a specific RFQ
GET/healthRelay health status

Health Check

curl https://relay.hyperquote.xyz/health

Response:

{ "status": "ok", "chainId": 999, "contractAddress": "0x...", "activeRfqs": 3, "totalQuotes": 12, "connectedClients": 5, "uptime": 3600 }

Configuration

Environment VariableDefaultDescription
RELAY_PORT8080WebSocket and REST port
RFQ_TTL_SECS60RFQ time-to-live before automatic cleanup
RATE_LIMIT_PER_MIN30Maximum messages per minute per IP
CHAIN_ID31337EIP-712 chain ID for signature verification
SPOT_RFQ_ADDRESSSpotRFQ contract address for EIP-712 domain

Rate limiting applies to all WebSocket message types including PING. Exceeding the limit (default 30 msg/min per IP) results in an ERROR response and dropped messages until the window resets.

Last updated on