Trade: Get Started
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/*.
What is needed first
Before beginning, ensure the following are in place:
- A wallet approved as a
TAKERon the network. Onboarding grants the role; see API → Get Started (~4 min). - A desk address to direct the request to. Read one from
GET /makers. - Test USDC deposited into the safe and a small amount of Base Sepolia ETH for gas. See Fund the Account (~3 min).
- The relayer base URL and the deployed core address. Both are in the API reference (~4 min).
Pick a desk
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.
Open the request for quote
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
pair, // keccak256 of "USD/PHP"
tenor_days: 7,
notional: notionalWad, // 100000 * 1e18, as a string
direction: "long",
quote_window_secs: 600,
expiry: Math.floor(Date.now() / 1000) + 600, // unix seconds the RFQ expires
}),
}).then((r) => r.json());
const rfqId = rfq.rfq_id ?? rfq.rfqId;The response returns an rfqId, and CRX begins pricing the request.
Wait for CRX to price and sign
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() : null));
if (!bundle) await sleep(1000);
}When the loop exits with a non-null bundle, CRX has priced and signed the request and the agreed quote is anchored on-chain.
Sign the Terms
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: "Terms",
message: terms,
});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.
Bind on-chain
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.
Allocate margin and read the position
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.
What is in place now
A bound NDF now stands, margined and marked continuously. From here:
- VM clears P&L into the safe every cycle, automatically; there are no margin calls — the position stays fully collateralized, and only below the maintenance margin is it closed. See The Margin Engine (~3 min).
- Read marks and PnL: Read the Data (~4 min).