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:
- Receive partial
OptionDetails— every term except the premium. - Decide what premium you'd pay (or skip).
- Submit a quote with the completed details.
- 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.