> ## Documentation Index
> Fetch the complete documentation index at: https://docs.joyride.exchange/llms.txt
> Use this file to discover all available pages before exploring further.

# TypeScript Maker SDK

> Quote Joyride RFQs from TypeScript — the quote runner, the signed package quote, nonce handling, and fill reconciliation.

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](/market-makers/rfq-quoting), what is signed and how it is
verified is on [The Signed Quote](/market-makers/quote-payload), and the
session and rate rules are on
[Connectivity for Bots](/market-makers/connectivity).

Quoting requires the `rfq_quoter` role on a provisioned account. See
[Becoming an RFQ Quoter](/market-makers/becoming-a-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](mailto: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.

```bash theme={null}
joyride rfq maker watch \
  --ttl 20 \
  --maker-subaccount <your-quoting-subaccount> \
  --price-command 'node price.js'
```

A skeleton that runs as written, and declines until you plug a model into it:

```js theme={null}
// price.js — one open request in on stdin, one answer out on stdout.
let input = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => {
  input += chunk;
});
process.stdin.on('end', () => {
  const request = JSON.parse(input);

  // Legs are ordered and signed, taker-buy orientation: a positive quantity is
  // a leg the taker receives when it buys the package.
  const contracts = request.legs.reduce(
    (total, leg) => total + Math.abs(Number(leg.quantity)),
    0,
  );
  if (contracts > 25) {
    process.stdout.write(JSON.stringify({ decline: 'size_limit' }));
    return;
  }

  const mid = pricePackage(request.legs);
  if (mid === null) {
    process.stdout.write(JSON.stringify({ decline: 'low_confidence' }));
    return;
  }

  // Whole-package totals, not per contract. Six decimal places.
  process.stdout.write(
    JSON.stringify({
      bid: (mid - 0.5).toFixed(6),
      ask: (mid + 0.5).toFixed(6),
    }),
  );
});

/** Replace with your model. Returning null declines the request. */
function pricePackage(legs) {
  return null;
}
```

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.

| Field                | Meaning                                                      |
| -------------------- | ------------------------------------------------------------ |
| `bid`                | Signed whole-package total you **pay** if the taker sells    |
| `ask`                | Signed whole-package total you **receive** if the taker buys |
| `max_fee`            | The most maker fee you authorize on this fill                |
| `max_locked_balance` | The most your post-fill locked balance may reach             |
| `expires_at`         | When the quote stops being firm                              |
| `quote_nonce`        | Monotonic per account, see below                             |

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](/market-makers/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`:

```ts theme={null}
interface RfqNonceProvider {
  read(wallet: string): Promise<string | null>;
  /** Persisted BEFORE the quote is sent, never after. */
  write(wallet: string, nonce: string): Promise<void>;
}
```

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:

```ts theme={null}
const runner = new MakerQuoteRunner({
  price: (request) => pricePackage(request),
  ttlSeconds: 20,
  makerSubaccount: '<your-quoting-subaccount>',
  nonces: myNonceProvider,
  feeHeadroomBps: 50,
  lockHeadroomBps: 50,
  session: client.rfq.maker,
  fills: client.rfq.reads,
  connection: socket,
});

await runner.start();
```

`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](/agents/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](/market-makers/fees-and-limits).

## Support

Onboarding, the quoter role, and library access:
[support@joyride.exchange](mailto:support@joyride.exchange).
