This guide walks through placing a working hedge. The taker requests a quote, accepts it — two signatures bind it — and sees the live position.
Follow the steps in order: nothing here branches, and each call sets up the next.
For the concepts behind the flow, see Request for Quote (~2 min).
NoteNo login on the trade path. Quoting and binding are authenticated by the two Terms signatures, not a session. Trading does not call/auth/*.
Before beginning, ensure the following are in place:
TAKER on the network. Onboarding grants the role; see API → Get Started (~4 min).GET /makers.In this step, the taker chooses the desk the request goes to. Call GET /makers; it returns the desk registry, and the caller picks one address from it:
const makers = await fetch(`${RELAYER}/makers`).then((r) => r.json());
const maker = makers[0].address;maker now holds the address of the desk the request for quote is directed to.
In this step, the taker opens the request for quote: call POST /rfq with the pair, notional, direction, tenor, and the chosen desk, and the relayer carries the request to CRX.
To read the cost before committing, call GET /margin?pair=¬ional= first; it returns the vol-scaled initial margin and changes nothing.
Open the request using the following call:
const rfq = await fetch(`${RELAYER}/rfq`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-CRX-Address": taker },
body: JSON.stringify({
counterparty: maker,
aca_number: "0", // your existing ACA with that maker
instrument: "0", // NDF
The response returns an rfqId, and CRX begins pricing the request.
CRX prices the request and signs the Terms. When that confirm lands, the relayer anchors the agreed quote on-chain. Poll GET /rfq/:id/bundle until it returns the dual-ready bundle:
let bundle = null;
for (let i = 0; i < 12 && !bundle; i++) {
bundle = await fetch(`${RELAYER}/rfq/${rfqId}/bundle`, {
headers: { "X-CRX-Address": taker },
}).then((r) => (r.ok ? r.json() :
When the loop exits with a non-null bundle, CRX has priced and signed the request and the agreed quote is anchored on-chain.
In this step, the taker countersigns. The bundle carries the canonical Terms and CRX's signature; sign the same Terms with the taker wallet using EIP-712. The domain is { name: "CRX", version: "1", chainId, verifyingContract }.
Sign the Terms with the following call:
const terms = termsFromWire(bundle.terms);
// terms.lockedRate - the forward rate CRX stands behind
// terms.imLongBps / terms.imShortBps - the margin each side posts:
// IM owed = notional × imBps / 10_000
// terms.sigDeadline - bind before this, or re-request
const takerSig = await taker.signTypedData({
domain: { name: "CRX", version: "1", chainId: 84532, verifyingContract: CRX },
types: TERMS_TYPES,
primaryType:
takerSig now holds the second of the two signatures the bind requires. The exact Terms field order is in Accepting a Quote (~2 min).
WarningDo not bind a quote whosesigDeadlinehas passed. Re-request instead.
Submit openAndBind with both signatures; one transaction opens the Account Control Agreement and binds the first position. The Account Control Agreement (ACA) is the container that pairs the taker with CRX — it holds the positions and the segregated collateral, under the Terms both sides signed.
Bind the trade using the following call:
await crx.write.openAndBind([
taker, maker, 0n, zeroAddress, terms, takerSig, bundle.maker_signature,
]);When the transaction confirms, the ACA is open and the position is bound.
In this step, the taker funds the position. Move the initial margin from the safe into the SCA for this agreement, then read it back:
await crx.write.allocate([acaId, USDC, imTaker]);
const sca = await crx.read.sca([acaId, taker, USDC]);The sca read returns the initial margin posted. The position is live.
A bound NDF now stands, margined and marked continuously. From here: