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 Connectivity for Bots. 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 that runs as written, and declines until you plug a model into it:
Three answers are possible and all three are answers: a firm {bid, ask}, an explicit {"decline":"<reason>"}, or empty output, which declines with --decline-reason. 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 both sides of the whole package. When your pricing function or --price-command supplies the two maxima itself, the keys are maxFee and maxLockedBalance (the wire names are 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 the taker side’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. Read it from joyride rfq account --output json; hex and base58 are both accepted.

The quote nonce

The venue keeps a per-account quote nonce high-water mark, and it survives restarts. A maker that seeds its nonce from memory on each boot will collide with that mark and have its first quote refused with RFQ_STATE_CONFLICT. 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 fill. The same fill arrives as confirmed, then as finalized or reverted. A reverted fill did not happen, so unwind anything you hedged on it. Key your handler on the fill’s rfq_id and quote_id and act on finality, rather than counting calls. Give the runner a fill reader and a disconnect signal and it reconciles for you: after a reconnect it re-reads recent maker fills and brings its view back in line. The underlying read is GET /api/query/rfq-fills?role=maker with your bearer token, newest first, cursored by the last row’s filled_seq_no. 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 refused rather than sent: it would be firm but untakeable.
  • Losing quoters are not notified. The first firm quote wins and later quotes expire quietly, 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:
price may return a firm {bid, ask}, 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 priced, quoted, declined, dropped, unresolved, and errored requests — every branch the loop can take, so an operator can scrape them. 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. Read the request back instead.

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.