Skip to main content
An RFQ is a package of one to thirteen option legs on one underlying and one expiry. The taker submits the legs and names a side: buy or sell, which reveals the direction and asks for that one price, or two_way, which hides it and takes a bid, an ask, or both. The first firm quote wins, and the taker accepts a side the quote priced. This page covers the maker methods and pushes. It assumes the quoter role and the readiness checks on Becoming an RFQ Quoter. Session mechanics are on API Connectivity; what the signature covers is on The Signed Quote. Each method and push has a reference page with its full schema and an example frame under Reference in this tab.

Summary

Lifecycle

A request is first visible as pending. States after quoted are delivered only to the maker whose quote won. After another quoter wins, the request is no longer visible. A late rfq.maker.respond returns RFQ_STATE_CONFLICT, and no push reports the outcome.

Connecting

The maker channel is per socket. On every connection, including a resume with public/session_resume, call rfq.maker.subscribe, then page rfq.maker.poll until a result has no next_cursor. Subscribing first closes the gap in which a request could open unseen. Poll items and open pushes have the same shape and overlap in time, so deduplicate on rfq_id. Errors are per call and leave the socket usable; a retryable error on either method is retried with backoff on the same connection. rfq.maker.poll, rfq.maker.respond, and rfq.maker.decline count against the 100-request account budget; rfq.maker.subscribe does not. See API Connectivity for the fixed-window semantics. Frames for rfq.maker.subscribe and rfq.maker.poll are on their reference pages. A poll result, which lists every request still open for quotes:
Pass next_cursor back as cursor. Paging ends when a result has no next_cursor.

The open request

After subscribing, each new request arrives as a subscription notification on the rfq_maker channel with type: "open" and an rfq object of the same shape as a poll item. The taker fixes every leg’s quantity when creating the request. An older one-leg rfq.create may send {instrument, size} instead of legs[]; the venue converts that pair to one positive leg. In either case, quote the exact legs[] shown here. A maker cannot change the quantity.

Pricing a package

bid and ask are signed USDC totals for the whole package in legs[], not per-contract or per-leg prices, and no size multiplier applies. Send the side(s) the request’s side allows: only ask on a buy request, only bid on a sell request, either or both on two_way. When both are present bid <= ask is required. Either side may be negative, since a credit structure or an inverted ratio can have negative package value. A side you leave out is signed as a reserved sentinel inside the payload (see The Signed Quote); that is the venue’s concern, and your JSON simply omits the field. The accepted side fixes both the cash and the contracts. Quantities are in taker-buy orientation: on a buy, the taker’s leg vector is legs[] as given and the maker’s is its negation. The same rules apply to negative prices. With bid: "-120.000000" and ask: "-80.000000", a taker who buys pays -80, so the taker collects 80 USDC from the maker. A taker who sells receives -120, so the taker pays 120 USDC to the maker. Example: 1×2 call spread. The request above asks for +1.000000 of the 77000 call and -2.000000 of the 80000 call. With marks of 1270 USDC and 400 USDC, the package is worth 1 × 1270 - 2 × 400 = 470 USDC to a buyer, and a quote 30 USDC either side of that value is bid 440, ask 500. premium on the fill is the accepted package total, not the maker’s signed cash flow: the maker receives it when maker_side is sell and pays it when maker_side is buy. maker_fee is 0 at launch; the fee formula and rates are on Fees and Limits. Either accepted side leaves the maker short at least one leg, so writers.maker on the fill is true.

Responding

A response is a signed quote on one side or both. The maker signs the quote with the wallet key bound to the account and sends the fields from which the venue rebuilds it. The venue verifies the signature over its own reconstruction and compares every field; any difference is RFQ_CANONICAL_MISMATCH. What the signature covers is on The Signed Quote. rfq.maker.respond carries no size; the taker’s requested quantities are already fixed in legs[]. A request that includes size, or any other field not listed below, is rejected with RFQ_INVALID_PARAMS. The frame is on rfq.maker.respond. The result carries the quote_id that identifies the signed quote in every later push. Declining. A decline ends the request. The venue moves it to declined at once, the taker sees the reason instead of waiting for the 5-second timeout, and any later rfq.maker.respond or rfq.maker.decline on it, from any quoter, returns RFQ_STATE_CONFLICT. Decline only requests that would not be quoted at any price, once per request. The result echoes rfq_id with state: "declined" and the decline_reason; the frame is on rfq.maker.decline. Quote rules.
  • expires_at is at most 3600 seconds after issued_at and no later than max_quote_expires_at from the open request. A later value is RFQ_VALUE_OUT_OF_RANGE; the venue never clamps.
  • A quote must reach the venue with more than 2 seconds of life left, or it is RFQ_QUOTE_EXPIRED. The taker’s accept and submission windows end 2 seconds before the quote’s expiry.
  • A quote is firm for its full TTL regardless of market moves, so a short, fixed TTL is the recommended policy.
  • quote_nonce is an unsigned 64-bit integer sent as a decimal string, and it must be higher than every nonce the account has ever sent, across restarts; a wall-clock seed at startup satisfies this. A nonce at or below the account’s high-water mark is refused with RFQ_STATE_CONFLICT.
  • max_maker_fee and max_maker_locked_balance are unsigned 6-decimal USDC strings covered by the signature. They must be at least the larger fee and post_fill_locked across the sides you quoted, recomputed by the venue at your signed bid and ask; a smaller value is RFQ_SIGNED_LIMIT_BELOW_REQUIREMENT, and a subaccount with no covered anchor is RFQ_MAKER_UNFUNDED. One pair covers every quoted side because the taker chooses among them; an unquoted side contributes nothing. What they authorize is on The Signed Quote.
  • At least one of bid and ask is required, and only the side(s) the request’s side allows. A supplied unrequested side is RFQ_SIDE_NOT_REQUESTED; the request stays open and a corrected quote may follow.
  • One quote per request.

After quoting

Three push types follow a winning quote, all on the rfq_maker channel and delivered only to the winning maker. Their frames are on rfq_maker pushes.
  • state arrives on every transition from the quote on: quoted, accepted, submitting (twice), expired, and failed. accepted_side is null until the taker accepts and then carries buy or sell. On a taker buy the maker writes every positive leg and the taker every negative one; on a taker sell the roles flip.
  • fill arrives when the fill lands. premium is the accepted package total before fees, the same value as execution_price: the quote’s ask when the taker bought, its bid when the taker sold. It is never negated for the maker, so read the direction from maker_side: on sell the maker receives premium, on buy the maker pays it, and a negative premium reverses that. Book the execution provisionally on this push, keyed by rfq_id and quote_id; a later re-land of the same accepted RFQ is not a second trade.
  • finality repeats the fill object with finality changed from confirmed to finalized, or to reverted if the chain dropped that transaction. finalized is terminal success. reverted is not terminal: the RFQ returns to submitting and the relayer may submit the same accepted trade again. Mark that transaction attempt reverted, but do not unwind an external hedge or declare the RFQ failed yet. Wait for either another fill for the same rfq_id and quote_id under a new tx_signature, in which case keep the existing hedge and do not hedge it a second time, or a state push with state: "failed", in which case the trade is terminally absent and the hedge can be unwound under the desk’s policy.
Losing and late quotes. A losing or late quote receives no push. If another quoter’s respond reached the venue first, the later respond returns RFQ_STATE_CONFLICT and nothing further is sent about the request. The error means another quoter won, and any reserved risk can be released. A respond that arrives after request_deadline returns the same error, because the request has drained. The only push every quoter receives after open is the broadcast state with declined and quote_id: null, sent when the window closes with no quote or a leg stops trading. Reserved risk should be released on local timers, one per request from request_deadline and one per delivered quote from expires_at, not on the arrival of a message.

Reconciliation

On reconnect, run the connect sequence above: rfq.maker.subscribe, then page rfq.maker.poll, which returns every request still inside its window. Missed fills are not replayed over the socket, so read them from the query API with the session token as a bearer token and group attempts by rfq_id and quote identity. A reverted row invalidates that transaction attempt, not necessarily the accepted RFQ: a later row for the same RFQ may be its re-land under a new signature. Book the active confirmed or finalized attempt once. If the newest known attempt is reverted and no later fill or terminal failed state was observed, the fill history alone does not prove failure; leave the hedge in a pending-reversion state and reconcile the outcome rather than unwinding automatically. Send no status filter: it matches one finality value exactly and would hide other attempts. Rows come newest first; while a page carries next_cursor, pass it back as after to read the next one. The row schema and filters are on GET /api/query/rfq-fills.

Errors

Every rfq.maker.* failure is JSON-RPC error 1200. data.error carries the RFQ code, data.retryable states whether the same request can succeed later, and data.operation_id identifies the call for support@joyride.exchange. RFQ_NOT_READY has several causes, distinguished by message and listed on Becoming an RFQ Quoter.

What is not supported

  • There is no cancel and no replace of a delivered quote. A quote is firm until expires_at, and a second rfq.maker.respond on the same request returns RFQ_STATE_CONFLICT. Re-pricing means letting the quote expire, so the TTL is the only control over firmness.
  • The first firm quote wins and every later quote is refused. rfq.maker.decline with a reason ends the request at once instead of at the 5-second timeout.
  • A losing or late quote receives no push. Reserved risk is released on local timers per request and per delivered quote.
  • There are no partial fills. A package fills for its exact legs[] or not at all; a request that cannot be filled whole is declined with size_limit.
  • There is no maker read of live quotes over the socket. Each delivered quote must be recorded locally. rfq.maker.poll on reconnect returns what is still open, and GET /api/query/rfq-fills?role=maker is the record of fills.
Coming from Deribit Block RFQ? Migrating from Deribit maps the two surfaces method by method. Next: The Signed Quote