Trade: Get Started
A working hedge. You request a quote, accept it (two signatures bind it) and see the live position.
Follow every step in order. Nothing here branches; each call sets up the next.
See How the RFQ flow works (~3 min).
NoteNo login on the trade path. Quoting and binding are authenticated by the two Terms signatures, not a session. You do not call/auth/*to trade.
What you need first
- A wallet approved as a
TAKERon the venue. 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 your general balance and a few S for gas. See Fund Your Account (~3 min).
- The relayer base URL and the deployed core address. Both are in the API reference (~4 min).
Pick a desk
Call GET /makers. It returns the desk registry. Pick one address to direct your request to.
const makers = await fetch(`${RELAYER}/makers`).then((r) => r.json());
const maker = makers[0].address;Open the request for quote
POST /rfq with the pair, notional, direction, tenor, and the desk you chose. The relayer carries it to CRX.
To read the cost before you commit, call GET /margin?pair=¬ional= first; it returns the vol-scaled initial margin and changes nothing.
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;You now hold an rfqId. CRX prices it.
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);
}Sign the Terms
The bundle carries the canonical Terms and CRX's signature. Sign the same Terms with your wallet (EIP-712). The domain is { name: "CRX", version: "1", chainId, verifyingContract }.
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,
});The exact Terms field order is in Binding & Terms (~5 min).
WarningDo not bind a quote whosesigDeadlinehas passed. Re-request instead.
Bind on-chain
Submit openAndBind with both signatures. This opens the Account Control Agreement and binds the first position in one transaction. The Account Control Agreement (ACA) is the container pairing you with CRX. It holds the positions, the segregated collateral, and the cure terms both sides signed.
await crx.write.openAndBind([
taker, maker, 0n, zeroAddress, terms, takerSig, bundle.maker_signature,
]);Allocate margin and read the position
Move your initial margin from general balance 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]);sca reads back your posted IM. The position is live.
What you have now
A bound NDF, margined, marked continuously. From here:
- VM clears your P&L into your general balance every cycle, automatically; a margin call fires only at a breach. See Variation Margin (~4 min).
- Read marks and PnL: Read Your Data (~4 min).