Backpack Convert/RFQ - Complete Guide
TL;DR
- RFQ = Request for Quote. You ask market makers "what's your price?" instead of trading on an order book.
- Convert flow: User enters amount → App sends RFQ → Market maker quotes → User accepts → Trade settles
- Two execution modes:
AwaitAccept: Get quotes, show to user, wait for them to click ConvertImmediate: Fallback when quote expired - auto-execute if price is within slippage limit
- Timeline: Market makers have 1 min to quote, RFQ expires after 2 min total
- Slippage: Default 1%, max 5%. Only applied in Immediate mode as a price limit.
- Fees: Baked into the displayed price (you see what you get)
- Convert ≠ Trading: Convert doesn't enable
autoBorrow- you can only swap what you have - Symbol format:
SOL_USDC_RFQ(USDC always quote, otherwise alphabetical) - Two-sided quotes: Market makers send both bid+ask, but each quote is bound to one specific RFQ
- WebSocket: Real-time quote updates via
rfqCandidateevents, matched byclientId - Scope: RFQ is ONLY used for Convert and system CollateralConversion - other liquidation flows use direct order book execution
Where RFQ Is (and Isn't) Used
RFQ is a focused system with two use cases:
| Use Case | Initiator | Purpose |
|---|---|---|
| User Convert | User via app | Simple token swaps with market maker quotes |
| CollateralConversion | System | Convert non-USDC assets to USDC for margin requirements |
What does NOT use RFQ:
| Flow | Execution Method |
|---|---|
| Position liquidations | Direct order book or backstop pool |
| Auto-deleveraging (ADL) | Matched against other users' positions |
| Futures expiry/settlement | Direct settlement orders |
| Order book closure | Direct cancellation orders |
The RFQ system exists specifically to provide better price discovery through market maker competition - either for user convenience (Convert) or for system efficiency (getting good prices when converting collateral).
What is RFQ?
RFQ = Request for Quote
Instead of trading against a public order book, you ask market makers directly: "I want to trade X. What's your price?"
You: "I want to sell 1 SOL"
Market Maker: "I'll give you $150 for it"
You: "Deal"Why Use RFQ/Convert Instead of an Order Book?
Benefits for Users
| Benefit | Order Book | RFQ/Convert |
|---|---|---|
| Price certainty | Market orders can slip | You see exact price before committing |
| Large orders | Eats through book, gets worse prices | MM quotes for your specific size |
| Partial fills | Can fill at multiple prices | All-or-nothing at quoted price |
| Simplicity | Need to understand order types | Enter amount, see price, click Convert |
| Hidden liquidity | Only visible book depth | MMs can offer better prices privately |
1. No Slippage Surprise
Order book (market order):
You want to buy 100 SOL
Order book has:
10 SOL @ $150.00
20 SOL @ $150.10
30 SOL @ $150.25
40 SOL @ $150.50
Your 100 SOL costs:
10 × $150.00 = $1,500.00
20 × $150.10 = $3,002.00
30 × $150.25 = $4,507.50
40 × $150.50 = $6,020.00
─────────────────────────
Total: $15,029.50 ($150.30 avg)
You expected ~$150, got $150.30 = 0.2% slippageRFQ/Convert:
You want to buy 100 SOL
Market maker quotes: $150.15
You see "$150.15" before clicking Convert
You get exactly $150.15 - no surprise2. Better Prices for Larger Orders
Market makers can offer size-specific pricing. For 1,000 SOL, they might:
- Check their inventory
- Hedge on other venues
- Offer a better price than the visible order book
On an order book, 1,000 SOL would destroy the book and you'd pay a premium.
3. Simplicity for Non-Traders
Order books require understanding:
- Limit vs market orders
- Bid/ask spread
- Order book depth
- Partial fills
- Time-in-force
Convert is just:
- Pick tokens
- Enter amount
- See price
- Click button
Trade-offs
| Downside | Explanation |
|---|---|
| Requires MM availability | If no market maker responds, no trade |
| Less transparency | You don't see full market depth |
| Potentially worse for tiny orders | Order book might beat MM spread for $10 trades |
| 2-minute expiry | Need to act within the window |
When to Use Each
| Use Case | Recommendation |
|---|---|
| Quick swap, any size | Convert |
| Large order (minimize slippage) | Convert |
| You want a specific limit price | Order book |
| You want to see market depth | Order book |
| Casual user | Convert |
| Active trader | Order book |
The Complete Convert Flow
┌─────────────────────────────────────────────────────────────────────────────┐
│ STEP 1: User enters amount │
│ ──────────────────────── │
│ User opens Convert, selects SOL → USDC, types "1" in the From field │
│ │
│ STEP 2: App submits RFQ (after 500ms debounce) │
│ ────────────────────────────────────────────── │
│ POST /api/v1/rfq │
│ { │
│ symbol: "SOL_USDC_RFQ", │
│ side: "Ask", // Selling SOL │
│ quantity: "1", // 1 SOL │
│ executionMode: "AwaitAccept" // Wait for quotes, don't execute yet │
│ } │
│ │
│ STEP 3: Backend broadcasts RFQ to market makers │
│ ─────────────────────────────────────────────── │
│ Market makers see: "Someone wants to sell 1 SOL" │
│ │
│ STEP 4: Market maker submits quote │
│ ────────────────────────────────── │
│ POST /api/v1/rfq/quote │
│ { │
│ rfqId: "abc-123", │
│ bidPrice: "149.50", // "I'll BUY at this price" │
│ askPrice: "150.50" // "I'll SELL at this price" │
│ } │
│ │
│ Backend picks bidPrice (149.50) because user is selling │
│ │
│ STEP 5: WebSocket pushes quote to app │
│ ───────────────────────────────────── │
│ { │
│ eventType: "rfqCandidate", │
│ rfqId: "abc-123", │
│ quoteId: "xyz-789", │
│ price: "149.50" │
│ } │
│ │
│ STEP 6: App displays quote to user │
│ ────────────────────────────────── │
│ From: 1 SOL │
│ To: 149.50 USDC │
│ Rate: 1 SOL = 149.50 USDC │
│ │
│ STEP 7: User clicks "Convert" │
│ ───────────────────────────── │
│ Option A - Quote still valid: │
│ POST /api/v1/rfq/accept { quoteId: "xyz-789" } │
│ │
│ Option B - Quote expired: │
│ POST /api/v1/rfq (Immediate mode with slippage limit) │
│ { ..., executionMode: "Immediate", price: "148.01" } │
│ │
│ STEP 8: Trade settles │
│ ──────────────────── │
│ Your account: -1 SOL, +149.50 USDC, -fee │
│ Market maker: +1 SOL, -149.50 USDC, -fee (or +rebate) │
│ │
│ STEP 9: WebSocket confirms fill │
│ ─────────────────────────────── │
│ { eventType: "rfqFilled", ... } │
│ │
│ App shows success screen │
└─────────────────────────────────────────────────────────────────────────────┘Timeline & Expiration
T=0:00 User submits RFQ
│
│ ← Market makers can submit quotes
│
T=1:00 ════════════════════════════════════
QUOTE SUBMISSION DEADLINE
════════════════════════════════════
│
│ ← NO new quotes allowed (rejected as "late")
│ ← User can still ACCEPT existing quotes
│
T=2:00 ════════════════════════════════════
RFQ EXPIRES
════════════════════════════════════
└─→ Everything cancelled- Market makers: 1 minute to respond
- User: 2 minutes total to accept
- App behavior: Auto-submits new RFQ when current one expires (keeps quotes fresh while user is on screen)
Execution Modes: AwaitAccept vs Immediate
There are only two execution modes:
pub enum RfqExecutionMode {
AwaitAccept, // Default: get quotes, wait for user to accept
Immediate, // Execute instantly if price is within limit
}When Each Mode Is Used
| Mode | When | Purpose |
|---|---|---|
AwaitAccept | User enters amount | "Get me a quote to display" |
Immediate | User clicks Convert BUT quote expired | "Execute now at my limit price" |
The Happy Path (AwaitAccept only)
1. User types "1 SOL"
→ App: submitRfq({ executionMode: "AwaitAccept" })
2. Quote arrives via WebSocket: "$150"
→ User sees the price
3. User clicks "Convert" (within 2 minutes)
→ App: acceptQuote({ quoteId }) ← Just accepting, NOT Immediate
→ SUCCESSImmediate mode is never used in this flow.
The Fallback Path (Immediate as safety net)
1. User types "1 SOL"
→ Quote arrives: "$150"
2. User gets distracted, goes to make coffee...
3. 2+ minutes pass, RFQ expires
4. User comes back, clicks "Convert"
→ App: acceptQuote({ quoteId })
→ FAILS: "Invalid RFQ identifier" (expired)
5. App automatically retries with Immediate mode:
→ submitRfq({
executionMode: "Immediate",
price: $148.50 ← calculated from slippage
})
→ Backend gets new quote, auto-accepts if price ≥ $148.50Decision Flow in Code
User clicks "Convert"
│
▼
Has quoteId?
┌───┴───┐
Yes No
│ │
▼ │
acceptQuote()│
│ │
Success? │
┌──┴──┐ │
Yes No │
│ │ │
▼ ▼ ▼
Done Error="Invalid RFQ"?
│
┌──┴──┐
Yes No
│ │
▼ ▼
submitImmediateRfq() Show error
│
▼
Execute with
slippage limitWhere's the "Limit Price" in Immediate Mode?
There's no separate UI field. The limit price is calculated automatically from:
- The last quoted price
- Your slippage setting (default 1%)
// ConfirmConvertScreen.tsx
const slippageMultiplier =
rfqSide === Side.Bid ? 1 + maxSlippage : 1 - maxSlippage;
const limitPrice = Big(price).times(slippageMultiplier).toFixed();
// Example (selling):
// price = $150, slippage = 1%
// limitPrice = $150 × 0.99 = $148.50Why Does Immediate Mode Exist?
UX safety net for the race condition between viewing a quote and clicking Convert:
❌ Without Immediate (bad UX):
User clicks Convert → "Quote expired, please try again" → Frustration
✅ With Immediate fallback (good UX):
User clicks Convert → Quote expired → Auto-retry → Success
User never knows the quote expiredCode References
Mobile - ConfirmConvertScreen.tsx (lines 221-259):
const handleConvert = async () => {
// Step 1: Try accepting existing quote
if (quoteId) {
try {
await acceptQuote({ data: { quoteId, rfqId } });
return; // Success!
} catch (error) {
if (errorMessage !== "Invalid RFQ identifier") {
Toast.show(`Conversion failed: ${errorMessage}`, "error");
return; // Real error, stop
}
// Expired, fall through to Immediate...
}
}
// Step 2: Fallback to Immediate mode
await submitImmediateRfq(limitPrice);
};Mobile - provider.tsx (lines 428-457):
const submitImmediateRfq = async (limitPrice: string) => {
await submitRfq({
data: {
quantity: params.quantity,
quoteQuantity: params.quoteQuantity,
symbol: params.rfqSymbol,
side: params.rfqSide,
price: limitPrice, // ← Slippage-adjusted limit
autoLendRedeem: true,
executionMode: "Immediate", // ← The key difference
},
});
};Why Two-Sided Quotes (Bid + Ask)?
Market makers submit BOTH prices, but the quote is tied to ONE specific RFQ:
Quote {
rfq_id: "abc-123", // Bound to THIS RFQ only
bid_price: 149.50, // "I'll BUY at this price"
ask_price: 150.50, // "I'll SELL at this price"
}The system picks the relevant price:
- User selling (Ask) → Maker buying → Use
bid_price - User buying (Bid) → Maker selling → Use
ask_price
Why not just one price?
- Simpler API for market makers (always send both)
- Symmetric payload structure
- The unused price is simply ignored
Important: A quote CANNOT be reused for a different RFQ. Each quote is bound to one rfq_id.
Slippage
What Is It?
A price cushion. "I'll accept up to X% worse than the quoted price."
Default & Limits
- Default: 1%
- Max configurable: 5%
How It's Applied
Only matters when using Immediate execution mode (quote expired, need to execute now):
Buying (Bid):
worstAcceptablePrice = currentPrice × (1 + slippage)
Example: Buying at $150 with 1% slippage
Worst price: $150 × 1.01 = $151.50
Trade executes ONLY if price ≤ $151.50Selling (Ask):
worstAcceptablePrice = currentPrice × (1 - slippage)
Example: Selling at $150 with 1% slippage
Worst price: $150 × 0.99 = $148.50
Trade executes ONLY if price ≥ $148.50Backend Validation
// bpx-backend/engine/src/engine/rfq/mod.rs
if rfq.side == Side::Bid && quote.ask_price > limit_price {
return false; // Too expensive, reject
}
if rfq.side == Side::Ask && quote.bid_price < limit_price {
return false; // Too cheap, reject
}Fees
Structure
| Party | Fee Type | Typical Value |
|---|---|---|
| User (Taker) | requester_fee | Positive (you pay) |
| Market Maker | quoter_fee | Can be negative (rebate) |
Fee Embedding
The price shown to users already includes their fee:
pub fn requester_price(&self, rfq: &RequestForQuote) -> Price {
let fee_adjustment = price × (taker_fee_bps / 10000);
match quote.side {
Side::Bid => price - fee_adjustment, // You get less
Side::Ask => price + fee_adjustment, // You pay more
}
}Example (selling SOL):
- Market maker's bid: $150.00
- Your fee: 10 bps (0.1%)
- Price shown to you: $149.85 (fee already deducted)
WebSocket Events
| Event | Meaning |
|---|---|
rfqActive | Your RFQ is live, waiting for quotes |
rfqCandidate | A quote arrived (shows the price!) |
rfqAccepted | You accepted a quote |
rfqFilled | Trade completed, balances updated |
rfqCancelled | RFQ cancelled or expired |
rfqRefreshed | RFQ time window extended |
Payload Structure
interface RfqUpdate {
eventType: string;
rfqId: string;
quoteId?: string;
price?: string; // The quote price
quantity?: string;
quoteQuantity?: string;
expiryTime?: number;
timestamp: number;
}Margin & Collateral
When Does Margin Matter?
| Situation | Margin Check? |
|---|---|
| Selling assets you fully own, no other positions | Minimal (just balance check) |
Selling with auto_borrow: true | Yes - need collateral for the borrow |
| You have open perp positions | Yes - your assets back those positions |
| You have existing borrows | Yes - your assets back those borrows |
Why Can't I Always Sell What I Have?
Backpack is a margin trading platform. Your assets aren't just sitting there - they might be collateral backing other positions.
Example:
You have: 10 SOL ($1500)
Open position: BTC-PERP requiring $1000 margin
You try to sell: 8 SOL ($1200)
After sale:
2 SOL left ($300)
Still need $1000 margin for BTC-PERP
Result: REJECTED (insufficient margin)Non-Obvious Features
1. The 20% Price Buffer (Soft Clear)
Problem: When you submit an RFQ, we don't know the final price yet. How do we check if you have enough margin?
Solution: Estimate conservatively using mark price + 20% buffer against you.
let multiplier = if rfq.side == Side::Bid {
1.2 // BUYING: assume you'll pay 20% MORE
} else {
0.8 // SELLING: assume you'll get 20% LESS
};
estimated_amount = quantity × mark_price × multiplier;Example (selling 1 SOL):
Mark price: $150
Buffer: 0.8 (selling)
Estimated proceeds: $150 × 0.8 = $120
System checks: "If they only get $120, is margin still OK?"
If yes → RFQ accepted (real price will likely be better)This is called "soft clear" because no funds are locked yet.
2. Auto-Lend/Borrow Integration
Four flags on every RFQ:
| Flag | What It Does |
|---|---|
auto_lend | Lend your proceeds after the trade |
auto_lend_redeem | Redeem existing lends to fund the trade |
auto_borrow | Borrow assets to complete the trade |
auto_borrow_repay | Use proceeds to repay existing borrows |
Example: You have 0.5 SOL but want to sell 1 SOL
With auto_borrow: true
→ System borrows 0.5 SOL
→ Executes trade (sell 1 SOL)
→ You now have USDC + a 0.5 SOL debt3. Collateral Conversion (Liquidations)
A system-initiated flow that converts non-USDC collateral to USDC when accounts are undercollateralized.
When it happens: Prediction market positions require 100% USDC collateralization. When an account's margin fraction falls below the initial margin fraction (IMF), the system converts other assets to USDC.
Two execution paths (chosen probabilistically via config):
| Path | How it works |
|---|---|
| On-book | Direct spot limit order (IOC) against the order book |
| Via RFQ | Submit RFQ with SystemOrderType::CollateralConversion, wait for market maker quote |
Why offer both?
- RFQ: Better prices for large conversions (market makers can quote tighter than thin order books)
- On-book: Faster execution, no waiting for market maker response
Example flow (via RFQ path):
Before:
Account has prediction position requiring $1500 USDC margin
SOL balance: 10 SOL
USDC balance: $0
Margin fraction: below IMF (undercollateralized)
System submits CollateralConversion RFQ:
→ Sells 10 SOL for $1500 USDC
Auto-settlement (unique to Backpack):
→ USDC automatically covers margin requirement
→ Any excess stays in account
After:
SOL balance: 0
USDC balance: $1500
Margin fraction: healthyMost exchanges require separate transactions. Backpack does it atomically.
See also: Liquidations Guide for full details on when/why CollateralConversion triggers, IMF calculations, and the liquidation state machine.
4. Best Quote Auto-Sorting
Quotes implement Rust's Ord trait and are stored in a BTreeSet:
impl Ord for Quote {
fn cmp(&self, other: &Self) -> Ordering {
match self.side {
Side::Ask => self.price().cmp(&other.price()), // Lower = better
Side::Bid => other.price().cmp(&self.price()), // Higher = better
}
}
}Result: Best quote is always first. No searching needed.
5. Only Best Quote Emits rfqCandidate
Multiple market makers can quote, but:
- All quotes stored in sorted set
- Only the best quote triggers a WebSocket event to the user
- User only ever sees the best price
- When accepted, worse quotes are auto-cancelled
6. Immediate Mode Auto-Accept
If you submit an RFQ with executionMode: Immediate and a limit price:
// Quote arrives, system checks immediately:
if quote.price <= your_limit_price {
// Auto-accept, no round-trip needed
execute_trade();
}Trade happens instantly if price is acceptable.
7. Refresh Cancels All Quotes
If you refresh an RFQ (extend its time):
- Time windows reset
- All existing quotes are cancelled
- Market makers must re-quote
Prevents gaming where makers submit lowball quotes hoping you accept before better ones arrive.
8. System RFQs
RFQs can be initiated by the system (not just users). These have special privileges:
pub struct RequestForQuote {
// ...
pub system_order_type: Option<SystemOrderType>, // None for user RFQs
// ...
}System RFQ privileges:
| Privilege | Why |
|---|---|
| Bypasses user permission checks | System-initiated, no user signature |
| Can execute during liquidation mode | Normal user RFQs blocked when account is liquidating |
Can use auto_lend_redeem | Only CollateralConversion system type has this privilege |
How account is determined:
let account_id = match payload.system_order_type {
Some(_) => payload.account_id, // System specifies explicitly
None => derive_from_signing_key, // User RFQs use signature
};Currently, CollateralConversion is the only system order type that uses RFQs. Other system order types (like LiquidatePositionOnBook) use direct order book execution.
Supported Tokens
Only 16 tokens can be converted (hardcoded whitelist):
AAVE, APT, BNB, BTC, DOGE, ETH, FOGO, HYPE, PAXG, SEI, SOL, SUI, USDT, XPL, XRP, USDC
USDC is always the "quote" currency (prices are in USDC terms).
Key Files
Mobile App (wallet/packages/app-mobile/src/app/exchange/)
| File | Purpose |
|---|---|
state/rfq-convert/atoms.ts | State atoms (amounts, quote, RFQ ID) |
state/rfq-convert/provider.tsx | RFQ submission, WebSocket listener, debouncing |
navigation/convert/ConvertScreen.tsx | Main convert UI |
navigation/convert/ConfirmConvertScreen.tsx | Confirmation & execution |
bottom-sheets/BottomSheetConvertSlippageAdjuster.tsx | Slippage settings |
Web App (bpx-web/src/components/modals/ConvertModal/)
| File | Purpose |
|---|---|
hooks/useConvertForm.tsx | All form logic, RFQ submission, WebSocket |
screens/ConvertFormScreen.tsx | Main input UI |
screens/ConvertConfirmScreen.tsx | Confirmation & execution |
screens/ConvertSelectTokenScreen.tsx | Token picker |
Backend (bpx-backend/)
| File | Purpose |
|---|---|
core/types/src/models/rfq.rs | RFQ & Quote data models |
api/src/routes/rfq/request.rs | Submit/accept/cancel RFQ endpoints |
api/src/routes/rfq/quote.rs | Submit quote endpoint |
engine/src/engine/rfq/mod.rs | RFQ matching logic, immediate execution |
engine/src/clearing_house/clearing/rfq.rs | Soft clear (20% buffer) |
engine/src/clearing_house/settlement/rfq.rs | Balance updates & fee deduction |
Convert Limitations (vs Trading)
Convert does NOT enable autoBorrow - you can only swap what you actually have.
// What Convert sends:
await submitRfq({
quantity,
symbol,
side,
autoLendRedeem: true, // ✅ Can redeem lends
// autoBorrow: ??? // ❌ NOT sent (defaults to false)
});| Feature | Convert | Spot Trading (Margin) |
|---|---|---|
autoLendRedeem | ✅ Yes | ✅ Yes |
autoBorrow | ❌ No | ✅ Yes (if margin enabled) |
autoBorrowRepay | ❌ No | ✅ Yes (if margin enabled) |
Why? Convert is designed as the "simple" flow. If you want to sell more than you have (shorting), use the Trade interface with margin enabled.
The backend fully supports autoBorrow on RFQs - it's just that the Convert UI doesn't enable it.
How the RFQ Symbol Is Constructed
The symbol format is: {BASE}_{QUOTE}_RFQ
Rules:
- If one token is USDC → USDC is always the quote
- Otherwise → alphabetically sorted
// From provider.tsx
const [baseSymbol, quoteSymbol] =
fromAsset === "USDC"
? [toAsset, fromAsset] // USDC is quote
: toAsset === "USDC"
? [fromAsset, toAsset] // USDC is quote
: fromAsset < toAsset
? [fromAsset, toAsset] // Alphabetical
: [toAsset, fromAsset];
const rfqSymbol = `${baseSymbol}_${quoteSymbol}_RFQ`;Examples:
- SOL → USDC:
SOL_USDC_RFQ - USDC → SOL:
SOL_USDC_RFQ(same! USDC always quote) - ETH → BTC:
BTC_ETH_RFQ(alphabetical)
Quantity vs QuoteQuantity
You can specify either the base amount OR the quote amount, not both.
| User Action | What's Sent | Meaning |
|---|---|---|
| Types in "From" field (SOL) | quantity: "1" | "I want to sell exactly 1 SOL" |
| Types in "To" field (USDC) | quoteQuantity: "150" | "I want to receive exactly $150" |
The system calculates the missing side based on the quote price.
// If user edited "From" (base asset)
quantity = fromAmount;
quoteQuantity = undefined;
// If user edited "To" (quote asset)
quantity = undefined;
quoteQuantity = toAmount;How Side (Bid/Ask) Is Determined
The side depends on which token you're selling and which is the base asset.
// From atoms.ts
// rfqSide is calculated based on:
// 1. Which field the user last edited
// 2. Which token is the base vs quote
// If selling the BASE asset → Ask
// If buying the BASE asset → BidExamples (SOL_USDC market, SOL is base):
- Selling SOL for USDC →
side: "Ask"(you're asking for USDC) - Buying SOL with USDC →
side: "Bid"(you're bidding for SOL)
ClientId: Matching WebSocket Events
The app generates a random clientId before each RFQ submission:
clientIdRef.current = Math.floor(Math.random() * 2147483647);
await submitRfq({
clientId: clientIdRef.current,
// ...
});When WebSocket events arrive, the app checks if the clientId matches:
createRfqUpdateListener((wsData: RfqUpdate) => {
// Only process events for OUR RFQ
if (wsData.clientId !== clientIdRef.current) {
return; // Not ours, ignore
}
// Process the update...
});This prevents processing stale events from old RFQs or other users.
Debouncing: Mobile vs Web
To avoid spamming the API while the user types:
| Platform | Debounce Time |
|---|---|
| Mobile | 500ms |
| Web | 1000ms |
// Mobile: provider.tsx
const DEBOUNCE_MS = 500;
// Web: useConvertForm.tsx
const DEBOUNCE_MS = 1000;The RFQ is only submitted after the user stops typing for this duration.
What If No Market Maker Responds?
If no market maker submits a quote:
- RFQ sits in "active" state
- User sees "Fetching quote..." indefinitely
- After 2 minutes, RFQ expires
- App auto-submits a new RFQ (if user is still on screen)
- Cycle repeats
There's no timeout error shown - the app just keeps trying.
In practice, market makers respond within milliseconds, so this is rare.
Backpack vs. Industry Standard
| Feature | Backpack | Typical RFQ |
|---|---|---|
| Quote format | Two-sided (bid+ask) | Single price |
| Auto-borrow/lend | Built-in | Separate flow |
| Fee visibility | Embedded in price | Shown separately |
| Best quote tracking | Auto-sorted BTreeSet | Manual comparison |
| Immediate execution | Auto-accept if within limit | Always wait for user |
| Liquidation integration | CollateralConversion (atomic) | Separate system |
| Quote on refresh | All cancelled | Varies |