Trade — Get Started
What you build here
A working hedge, end to end, from code. By the end you will have requested a quote from a maker, bound the terms with two signatures, and read your live position on-chain. This is the smallest complete taker integration.
Follow every step in order. Nothing here branches — each call sets up the next.
Testnet only. This runs on Sonic Testnet (chain 14601). No real value moves.
No 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 counterparty maker address to direct the request to. Read one from
GET /makers. - Test USDC deposited into your general balance and a few S for gas.
- The relayer base URL and the deployed core address. Both are in the API reference (~4 min).
Step 1 — Pick a maker
Call GET /makers. It returns the approved-maker registry. Pick one address to direct your request to.
const makers = await fetch(`${RELAYER}/makers`).then((r) => r.json());
const maker = makers[0].address;
Step 2 — Open the request for quote
POST /rfq with the pair, notional, direction, tenor, and the maker you chose. The relayer routes it to that maker's inbox.
const rfq = await fetch(`${RELAYER}/rfq`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-CRX-Address": taker },
body: JSON.stringify({
counterparty: maker,
pair, // keccak256 of "USD/PHP"
notional: notionalWad, // 100000 * 1e18, as a string
direction: "long",
tenor_days: 7,
quote_window_secs: 600,
}),
}).then((r) => r.json());
const rfqId = rfq.rfq_id ?? rfq.rfqId;
You now hold an rfqId. The maker sees it in their inbox.
Step 3 — Wait for the maker to price and sign
The maker 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);
}
Anchoring takes a few seconds. The bundle is not ready until the on-chain anchor confirms. Poll — do not give up after one call, or you will think no maker answered when one did.
Step 4 — Sign the Terms
The bundle carries the canonical Terms and the maker'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);
const takerSig = await taker.signTypedData({
domain: { name: "CRX", version: "1", chainId: 14601, verifyingContract: CRX },
types: TERMS_TYPES,
primaryType: "Terms",
message: terms,
});
The exact Terms field order is in Binding & Terms (~5 min).
Step 5 — Bind on-chain
Submit openAndBind with both signatures. This opens the Master Agreement and binds the first position in one transaction.
await crx.write.openAndBind([
taker, maker, 0n, zeroAddress, terms, takerSig, bundle.maker_signature,
]);
Step 6 — 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([maId, USDC, imTaker]);
const sca = await crx.read.sca([maId, 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 — no margin call. See Variation Margin (~4 min).
- Read marks and PnL: Get Data (~4 min).
Next: RFQ Flow (~4 min) — why the flow has the shape it does.