Skip to main content

Build an RFQ Quoter

This guide builds an RFQ quoter on the raw SDK: a process that listens for incoming RFQs, prices them, and submits signed quotes. The whole thing is about forty lines — the SDK handles offer construction, canonical hashing, signing, and wire encoding for you.

If you'd rather not write code at all, the CLI ships the same loop as a single command: overcast quote.

How a quoter fits the flow

A user creates an RFQ on the MAKER side (they'll write the option and post collateral). Your quoter registers as TAKER: it buys the option and pays the premium. Concretely, for each incoming RFQ you:

  1. Receive partial OptionDetails — every term except the premium.
  2. Decide what premium you'd pay (or skip).
  3. Submit a quote with the completed details.
  4. If the creator accepts, the backend settles the trade on-chain and tells you the resulting optionId.

Step 0: fund your vault

Premiums are paid from your escrow vault, not your wallet. A quoter that connects with an empty vault can receive RFQs but its accepted quotes will fail to settle — this is the most common integration mistake.

await app.chain.deposit({
depositor: app.publicKey,
mint: cashMint, // e.g. the USDC mint from app.api.listAssets()
amount: 10_000_000_000n,
});

Step 1: connect and register

import { OvercastApp, completeOptionDetails } from "@overcast/core";
import { SolanaFactory } from "@overcast/solana-client";

const app = await OvercastApp.create({
factory: new SolanaFactory({
rpcUrl: process.env.SOLANA_RPC_URL,
backend: process.env.OVERCAST_BACKEND_URL,
privateKey: process.env.OVERCAST_KEYPAIR,
}),
});

const rfq = app.rfq; // socket connects here
await rfq.registerRole("TAKER"); // quoters buy the option → TAKER side

Registration is per-connection; the client automatically re-registers your role whenever the socket reconnects.

Step 2: price and quote incoming RFQs

onNewRfq fires with the RFQ's curated partial details — asset addresses are already resolved to metadata (symbol, decimals, price hints), which is exactly what a pricing model needs:

rfq.onNewRfq = async (incoming) => {
// 1. Decide your premium (your pricing model goes here).
const premium = await priceRfq(incoming);
if (!premium) return; // skip RFQs you don't want

// 2. Fold the premium into the RFQ's terms → full, signable OptionDetails.
const details = completeOptionDetails(incoming.details, premium);
if (!details) return; // a leg was missing

// 3. Quote. The SDK builds your settlement offer, content-addresses it,
// signs the id, and sends it — salt and expiry are defaulted for you.
const { quoteId } = await rfq.submitQuote({
rfqId: incoming.rfqId,
details,
creator: app.publicKey,
side: "TAKER",
});
console.log("quoted", incoming.rfqId, "→", quoteId);
};

A premium is just:

const premium = {
premiumAsset: cashMint, // Id of the asset you pay in
premiumAmount: 12_500_000n, // native units, bigint
};

For a reference pricing model, see the CLI's built-in Black-Scholes pricer (packages/overcast-cli/src/commands/quote.ts): it identifies the cash leg vs the volatile leg, treats cash-settlement as a covered call and cash-collateral as a cash-secured put, derives the strike from the two amounts, and quotes fair value plus a spread.

Step 3: handle the rest of the lifecycle

rfq.onQuoteAccepted = (accepted) => {
// Your quote won — the option is already minted on-chain.
console.log("filled:", accepted.optionId, "rfq:", accepted.rfqId);
};

rfq.onRfqCancelled = ({ rfqId }) => console.log("cancelled:", rfqId);
rfq.onRfqExpired = ({ rfqId }) => console.log("expired:", rfqId);
rfq.onError = (err) => console.error(`rfq error [${err.code}]`, err.message);

process.on("SIGINT", () => {
rfq.disconnect();
process.exit(0);
});

Errors carry structured codes (RfqErrorCode): NOT_REGISTERED, WRONG_ROLE, VALIDATION_FAILED, RFQ_NOT_FOUND, RFQ_NOT_OPEN, QUOTE_NOT_FOUND, NOT_OWNER, ALREADY_ACCEPTED, BAD_SIGNATURE, SETTLEMENT_SUBMIT_FAILED, COLLATERAL_SUBMIT_FAILED, ACCEPT_OFFER_FAILED, INTERNAL. Requests that receive no acknowledgment reject after ackTimeoutMs (default 10 s).

The complete quoter

import { OvercastApp, completeOptionDetails } from "@overcast/core";
import { SolanaFactory } from "@overcast/solana-client";

const app = await OvercastApp.create({
factory: new SolanaFactory({
rpcUrl: process.env.SOLANA_RPC_URL,
backend: process.env.OVERCAST_BACKEND_URL,
privateKey: process.env.OVERCAST_KEYPAIR,
}),
});

const rfq = app.rfq;
await rfq.registerRole("TAKER");

rfq.onNewRfq = async (incoming) => {
const premium = await priceRfq(incoming); // your model
if (!premium) return;
const details = completeOptionDetails(incoming.details, premium);
if (!details) return;
const { quoteId } = await rfq.submitQuote({
rfqId: incoming.rfqId,
details,
creator: app.publicKey,
side: "TAKER",
});
console.log("quoted", incoming.rfqId, "→", quoteId);
};

rfq.onQuoteAccepted = (a) => console.log("filled:", a.optionId);
rfq.onError = (e) => console.error(`[${e.code}]`, e.message);

console.log("quoter running as", app.publicKey);

That's the entire integration. Everything else — offer structs, salts, expiries, canonical IDs, ed25519 signatures, bigint wire encoding — is the SDK's job.

After the fill

Once a quote is accepted you hold a live option position as the taker:

const option = await app.chain.getOption(collateralOfferId, settlementOfferId);
const claims = await app.chain.exerciseClaims(option, app.publicKey);

// Exercise: swap settlement for collateral, any amount up to your claims.
await app.chain.exerciseOption(option, claims);

The counterparty (maker) later calls redeemOption to collect their side. Track positions either through app.api.listOptions({ taker: app.publicKey }) or by consuming indexed chain events (OptionCreated, OptionExercised, OptionRedeemed) via the indexer interfaces.

The other side: creating RFQs

If you're building the requesting side instead (a wallet UI, a structured product), the flow is the mirror image:

await rfq.registerRole("MAKER");

const quotes = [];
rfq.onNewQuote = (q) => quotes.push(q);

const { rfqId } = await rfq.createRfq({
creator: app.publicKey,
request: { details }, // partial: no premium fields
side: "MAKER",
});

// ...collect quotes, pick the best one...
const { optionId, txHash } = await rfq.acceptQuote({
quoteId: best.quoteId,
side: "MAKER",
details: best.details, // the SDK signs your collateral offer
});

acceptQuote settles inline: when the promise resolves, the option is minted and txHash points at the settlement transaction.