Quote: Get Started
This guide walks through wiring a desk to the API. By the end, the desk has logged in to read its inbox, pulled a directed RFQ, priced it, signed the Terms, and watched the relayer anchor the quote on-chain.
Operating a desk (running it, risk-managing it, monetizing it) lives in Get Started. This page is the wire.
The following diagram shows the whole path, end to end:
Follow the steps in order; nothing branches.
NoteReads need a login; the quote itself is authenticated by the maker's signature. Login is required to read the inbox. The bind authority is the Terms signature produced in step 4.
What is needed first
Before beginning, ensure the following are in place:
- A wallet approved as a
MAKERon the network. See API → Get Started (~4 min). - Test USDC deposited into the safe, and gas. See Fund the Account (~3 min).
- The relayer base URL and the deployed core address.
Log in to read the inbox
The inbox is a private read. This step signs the desk in. Request a challenge, sign it with personal_sign, and exchange it for a one-hour token:
const { message } = await fetch(`${RELAYER}/auth/challenge`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address: maker }),
}).then((r) => r.json());
const signature = await wallet.signMessage({ message });
const { token } = await fetch(`${RELAYER}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ address: maker, signature }),
}).then((r) => r.json());The token now authenticates reads for the next hour.
Poll the inbox
Call GET /rfq/inbox?maker=<addr> with the token. Each item is an RFQ that CRX has routed to the desk, on a short expiry timer:
const inbox = await fetch(`${RELAYER}/rfq/inbox?maker=${maker}`, {
headers: { Authorization: `Bearer ${token}` },
}).then((r) => r.json());
const item = inbox[0];
const rfqId = item.rfq_id ?? item.rfqId;The rfqId is now ready to price.
WarningEach item expires in 30 seconds. Price it inside the window or it falls off the inbox.
TipTo decline, do nothing. There is no decline endpoint. An unpriced item drops off the inbox when its window passes, and nothing is owed.
Price the request
This step prices the request. Call POST /rfq/:id/quote with the rate and margin schedule. The call is unsigned: the relayer returns canonical Terms to sign.
Send the quote using the following call:
const priced = await fetch(`${RELAYER}/rfq/${rfqId}/quote`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({
quote: {
rfq_id: rfqId,
taker: item.taker, // echo the RFQ's identity fields from the inbox
maker,
aca_number: item.aca_number,
instrument: item.instrument, // NDF = "0"
pair: item.pair,
locked_rate: rate, // quote-ccy per USD, scaled
im_taker_bps: 200,
im_maker_bps: 100,
threshold_bps: 0,
mta_bps: 10,
tol_bps: 50,
valid_until: nowSecs + 600,
quote_nonce: String(nowSecs),
},
}),
}).then((r) => r.json());
const terms = termsFromWire(priced.terms);terms now holds the canonical Terms to sign in the next step.
Sign the Terms and confirm
Sign the returned Terms using EIP-712, then call POST /rfq/:id/confirm with the signature. On confirm, the relayer anchors the quote on-chain:
const makerSig = await wallet.signTypedData({
domain: { name: "CRX", version: "1", chainId: 84532, verifyingContract: CRX },
types: TERMS_TYPES,
primaryType: "Terms",
message: terms,
});
await fetch(`${RELAYER}/rfq/${rfqId}/confirm`, {
method: "POST",
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
body: JSON.stringify({ signature: makerSig }),
});Once the confirm lands, the quote is anchored on-chain.
TipQuote-lock holds while the quote is live. The maker stands behind the signed rate until itsvalid_untilpasses. If the market moves past the price, let it expire. Do not re-price under a live quote.
Allocate margin once bound
When CRX binds, post the maker's side initial margin into the agreement's SCA:
await crx.write.allocate([acaId, USDC, imMaker]);The maker's side initial margin is now posted.
To withdraw free collateral, deallocation runs in two phases:
await crx.write.computeDeallocation([acaId, tokens, amounts]);
await crx.write.deallocate([acaId, token, amount]);computeDeallocation snapshots the free margin; deallocate executes the withdrawal.
WarningDeallocation is exact and time-boxed. Snapshot the free margin, then execute within the five-minute window or it reverts as stale.
What the desk has now, and the branch
After the confirm, one of two outcomes follows:
- It binds. The taker fetched the bundle, signed the same Terms, and submitted the bind inside
sigDeadline— the position opens atomically, both sides in one transaction. The maker allocates; the position is live and marks for and against the maker continuously. - It expires. The taker did not bind before the window closed. The quote dies; nothing is owed and nothing is posted. The next poll shows a fresh inbox.
From here:
- The inbox model in full: Answer an RFQ (~6 min).
- How the collateral is posted and held: Manage the book (~5 min).
Desk operations
Beyond quoting, a desk owner calls these directly.
Setting up a desk
Onboarding is off-chain signing plus one polling call. Verify the whitelist with a public read, then poll the inbox:
GET /makers
→ [ { "address": "0x…your address…" }, … ]
GET /rfq/inbox?maker=0x…your address…
Authorization: Bearer <jwt>
→ [] // empty until CRX routes an RFQ to you
Assigning roles
Set the signer that acts for the desk by authenticating its wallet; rotate by authenticating a new wallet and discarding the old JWT:
POST /auth/challenge { "address": "0x…signer…" }
POST /auth/login { "address": "0x…signer…", "signature": "0x…" }
→ { "token": "eyJ…", "expires_at": … }
Acting as a sentinel
Poll the firm state to read live exposure before stepping in on a short counterparty:
GET /firm/0x…your address…
→ { "balance": "…", "open_acas": … }