Skip to main content
This page is for a desk that wants to quote Joyride from TypeScript rather than from raw WebSocket frames. It covers the quote runner, what goes into a signed package quote, how the quote nonce is kept, and how fills are reconciled after a disconnect. It does not restate the wire. The request, quote, and fill objects are on Quoting RFQs, what is signed and how it is verified is on The Signed Quote, and the session and rate rules are on API Connectivity. Quoting requires the rfq_quoter role on a provisioned account. See Becoming an RFQ Quoter.

The runner

Everything below is driven by one component, MakerQuoteRunner. You supply a pricing function; it owns subscribing to the request channel, catching up on open requests, pacing its polls inside the account’s rate budget, de-duplicating requests it has already answered, dropping requests whose window has closed, signing, persisting the quote nonce before each send, and reconciling fills after a reconnect. Note what is absent. Nothing in the runner pulls a delivered quote back.

Two ways to run it

The runner ships inside the joyride-cli package, and joyride rfq maker watch is that runner with your pricing function as a subprocess. This is the path that is installable today, and it is a real production path rather than a demo: a pricing program in TypeScript, Python, or anything else that reads and writes JSON plugs straight in. The library itself — for embedding the runner in your own long-lived TypeScript process — is distributed to onboarded quoters rather than published to a public registry. Ask for it at support@joyride.exchange when you are ready to run in-process.

Pricing as a subprocess

--price-command runs your program once per open request, with the request as JSON on stdin and your answer as JSON on stdout.
A skeleton showing the contract. Until pricePackage returns a value it declines every request it receives, and each decline is an answer sent to a trader, so run it against live requests only once the model is in:
Three answers are possible and all three are answers: a firm price, an explicit {"decline":"<reason>"}, or empty output, which declines with --decline-reason. A firm price is {bid, ask} on a two_way request (either key alone is also a firm price there), {ask} alone on a buy request, and {bid} alone on a sell request; the request’s side field says which. A price on a side the request did not ask for is declined low_confidence by the runner and never sent, since the venue would refuse it. A program that throws, exits non-zero, or overruns --price-timeout is treated as a decline too. A pricing bug must never wedge the loop, and the taker is waiting inside a five-second window. Decline reasons a maker may send are unlisted_instrument, near_expiry, size_limit, stale_data, low_confidence, and queue_overflow. A record can also carry timeout or leg_not_tradable, but those are the venue’s to emit; the gateway refuses them from a maker.

The signed package quote

One quote answers one request with the side or sides of the whole package the request asked for. A side you leave out is signed as a reserved sentinel by the SDK and omitted from the wire; you never write the sentinel yourself. When your pricing function or --price-command supplies the two maxima itself, the keys are maxFee and maxLockedBalance; the runner sends them as max_maker_fee and max_maker_locked_balance. A supplied value that does not parse is declined, never replaced with a derived one. Both totals are signed, and a sign is not decoration: a negative ask means the taker is paid to buy the package, which is cash leaving your vault. The most the package can take out of your vault is therefore max(bid, -ask, 0), not the larger magnitude of the two. The fee is on top of that, and it is owed whichever side the taker takes. So the most a quote can cost you is max(bid, -ask, 0) + max_fee, and that total is what an operator ceiling bounds — on the CLI’s joyride rfq maker watch, on the MCP maker tools, and on joyride rfq accept’s --max-premium. A package whose premium comes towards you is not automatically under a ceiling: its premium contributes nothing to the cost and its fee is the whole of it. The fee is also charged and capped per leg, so it grows with the number of legs while the premium does not have to; see Fees and Limits. Totals are whole-package, never per contract, and are canonical six-decimal strings. Do not compute them in floating point. The two maxima are the maker’s own guards inside the signed bytes. The runner derives them from the venue’s stated requirement on the request plus the headroom you configure, or you can return them yourself. When the request carries no computed funding view, the runner declines with stale_data; it never reads a missing maximum as zero. Every signed quote also carries your quoting subaccount address, copied in verbatim. It is not derived, because a wrong address signs bytes the venue refuses. The in-process SDK’s makerSubaccount value must be the 32-byte address encoded as base58. joyride rfq account --output json reports the same 32 bytes as 64 hexadecimal characters, so decode that hex and base58-encode the bytes before constructing MakerQuoteRunner. Do not pass the hex string to the SDK. The joyride rfq maker CLI commands are a separate convenience layer: their --maker-subaccount flag and JOYRIDE_RFQ_MAKER_SUBACCOUNT variable accept either encoding and normalize hex to base58 before calling the SDK.

The quote nonce

The venue keeps a per-account quote nonce high-water mark, and it survives restarts. A counter that restarts from a low value on each boot collides with that mark and has its first quote refused with RFQ_STATE_CONFLICT; a wall-clock seed does not, which is what the runner falls back to when it has no provider. The nonce is persisted before a quote is sent, never after. The CLI writes it to ~/.joyride/rfq-maker-nonce.json with owner-only permissions, through a temp file and a rename, and a damaged file is a loud configuration error naming the file rather than a silent restart of the series. In-process, the same contract is an RfqNonceProvider:
One account gets one nonce allocator. Two runners sharing an account and not a provider will fight over the series.

Reconciliation

The maker channel delivers fill and finality pushes over your own connection. A disconnect can drop them, and a fill that happened while you were away still happened. onFill fires once per finality stage, not once per economic trade. A fill arrives as confirmed, then as finalized or reverted. finalized is terminal success. reverted means that transaction attempt left the canonical chain, but it is not terminal for the accepted RFQ: the RFQ returns to submitting, and the relayer may land the same trade again under a new tx_signature. Key hedge state on rfq_id and the quote identifier, with the transaction signature as an attempt identifier. A live push carries quote_id, a base58 tx_signature and maker_side; a reconciled REST row carries the same quote hash as quote_hash, a hex tx_signature and the taker’s numeric side, so normalize both to one key before comparing. On reverted, mark the attempt reverted but do not unwind the hedge yet. A later confirmed fill for the same RFQ is a re-land, not a second trade, so retain the hedge rather than placing another one. Unwind only after the RFQ emits state: "failed"; on finalized, retain the hedge and clear the pending-finality state. MakerQuoteRunner.onFill forwards fill and finality objects, not state events, so also observe client.rfq.maker.onEvent for the terminal failed state. Give the runner a fill reader and a disconnect signal and it reconciles the common case: one second after each disconnect it reads the newest 100 maker fills, one page, and forwards any it had not seen. It does not page further, and it does not reconcile on startup or when the socket comes back, so a desk that can miss more than 100 fills, or that restarts, should read GET /api/query/rfq-fills?role=maker itself with its bearer token, newest first, cursored by the last row’s filled_seq_no. A reverted row alone does not prove that the RFQ ultimately failed; check for a later active attempt for the same RFQ and keep an unresolved reversion out of automatic hedge-unwind logic. For a strategy fill, legs is the authoritative package. Each leg includes instrument_registry_id, the canonical symbol, and its signed six-decimal quantity. The scalar instrument_registry_id, symbol, and size fields are compatibility fields for one-leg fills and are null for a multi-leg package. Two traps are worth naming. The side on a fill row is the taker’s side, so a desk that renders it as its own direction prints the opposite of what happened. And the server clamps the page limit, so a page shorter than you asked for does not prove you have reached the end; page until a page comes back empty.

There is no cancel and no replace

A delivered quote is firm until its expires_at. There is no method that pulls it back, amends it, or replaces it, and none is planned. The TTL you set is the only control over how long you are committed. This is the difference that costs money if it is ported over rather than designed for. A quoting loop built for a venue with cancel-and-replace treats a stale price as recoverable: it sends wide, then tightens or withdraws as the market moves. Here, the price you sent is live against you until it expires, so the exposure you accept is the full TTL multiplied by everything the underlying can do in it — and on an adverse move you will be taken on every quote you left standing. Practical consequences:
  • Choose a TTL you are willing to be held to on your worst tick, and treat lengthening it as a risk decision, not a convenience.
  • The runner requires the TTL explicitly and has no default, for this reason.
  • The runner clamps a TTL down to the request’s own ceiling (max_quote_expires_at), never up; the venue rejects a later expiry rather than clamping it, and caps any quote’s life at one hour. Shortening firmness is always safe.
  • A quote that would reach the venue with under two seconds of life left is not sent; the runner declines the request with near_expiry instead, since a quote that short would be firm but untakeable.
  • Losing quoters are not notified. The first firm quote wins and a later respond is refused with RFQ_STATE_CONFLICT, so keep a local expiry timer per quote to release reserved risk.

Embedding the runner

For an in-process desk, the runner is configured once and started:
Three of those values have requirements the types do not spell out. session must come from a client composed with the same nonces provider and a signer, or the constructor refuses to start a nonce series held in memory. fills is a MakerFillReader, an object exposing getRfqFills; the client’s read surface satisfies it structurally, which is how the CLI passes it. connection is anything exposing onDisconnected(handler). price may return a firm price ({ask}, {bid}, or {bid, ask} as the request’s side allows), a {decline}, or null to decline with the configured reason, and may be synchronous or asynchronous. stop() detaches cleanly, whenIdle() waits for in-flight pricing to drain, and stats() exposes counters for every branch the loop can take, so an operator can scrape them: priced, quoted, declined, dropped (window already closed), filtered (outside your assets), unresolved, errors, and fatal, which holds the configuration error that stopped the loop, or null. One counter deserves attention. Unresolved means a quote left your process with an unknown outcome, and it is never re-sent. A blind retry either gets refused or puts a second firm commitment on one request. A maker cannot read a request or quote back, so treat the quote as live until its expires_at and check GET /api/query/rfq-fills?role=maker for a fill.

Symbols from Deribit

Deribit-style instrument names are translated for you, in both directions. The translation is lexical and total: a symbol that cannot be carried across exactly is refused rather than guessed, because a silently wrong instrument is a wrong trade. If you are porting a Deribit Block RFQ quoting loop rather than writing a new one, read Migrating from Deribit first. It sets the two surfaces side by side, names what has no counterpart here, and covers the traps that do not announce themselves — the pricing output shape and the expiry units above all.

Fees

Fee rates are not restated here. The live schedule, the tiers, and the limits are on Fees and Limits.

Support

Onboarding, the quoter role, and library access: support@joyride.exchange.