Skip to content

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 Convert
    • Immediate: 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 rfqCandidate events, matched by clientId
  • 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 CaseInitiatorPurpose
User ConvertUser via appSimple token swaps with market maker quotes
CollateralConversionSystemConvert non-USDC assets to USDC for margin requirements

What does NOT use RFQ:

FlowExecution Method
Position liquidationsDirect order book or backstop pool
Auto-deleveraging (ADL)Matched against other users' positions
Futures expiry/settlementDirect settlement orders
Order book closureDirect 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

BenefitOrder BookRFQ/Convert
Price certaintyMarket orders can slipYou see exact price before committing
Large ordersEats through book, gets worse pricesMM quotes for your specific size
Partial fillsCan fill at multiple pricesAll-or-nothing at quoted price
SimplicityNeed to understand order typesEnter amount, see price, click Convert
Hidden liquidityOnly visible book depthMMs 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% slippage

RFQ/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 surprise

2. 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:

  1. Pick tokens
  2. Enter amount
  3. See price
  4. Click button

Trade-offs

DownsideExplanation
Requires MM availabilityIf no market maker responds, no trade
Less transparencyYou don't see full market depth
Potentially worse for tiny ordersOrder book might beat MM spread for $10 trades
2-minute expiryNeed to act within the window

When to Use Each

Use CaseRecommendation
Quick swap, any sizeConvert
Large order (minimize slippage)Convert
You want a specific limit priceOrder book
You want to see market depthOrder book
Casual userConvert
Active traderOrder 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:

rust
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

ModeWhenPurpose
AwaitAcceptUser enters amount"Get me a quote to display"
ImmediateUser 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
   → SUCCESS

Immediate 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.50

Decision 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 limit

Where'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%)
typescript
// 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.50

Why 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 expired

Code References

Mobile - ConfirmConvertScreen.tsx (lines 221-259):

typescript
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):

typescript
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:

rust
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.50

Selling (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.50

Backend Validation

rust
// 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

PartyFee TypeTypical Value
User (Taker)requester_feePositive (you pay)
Market Makerquoter_feeCan be negative (rebate)

Fee Embedding

The price shown to users already includes their fee:

rust
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

EventMeaning
rfqActiveYour RFQ is live, waiting for quotes
rfqCandidateA quote arrived (shows the price!)
rfqAcceptedYou accepted a quote
rfqFilledTrade completed, balances updated
rfqCancelledRFQ cancelled or expired
rfqRefreshedRFQ time window extended

Payload Structure

typescript
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?

SituationMargin Check?
Selling assets you fully own, no other positionsMinimal (just balance check)
Selling with auto_borrow: trueYes - need collateral for the borrow
You have open perp positionsYes - your assets back those positions
You have existing borrowsYes - 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.

rust
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:

FlagWhat It Does
auto_lendLend your proceeds after the trade
auto_lend_redeemRedeem existing lends to fund the trade
auto_borrowBorrow assets to complete the trade
auto_borrow_repayUse 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 debt

3. 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):

PathHow it works
On-bookDirect spot limit order (IOC) against the order book
Via RFQSubmit 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: healthy

Most 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:

rust
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:

rust
// 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:

rust
pub struct RequestForQuote {
    // ...
    pub system_order_type: Option<SystemOrderType>,  // None for user RFQs
    // ...
}

System RFQ privileges:

PrivilegeWhy
Bypasses user permission checksSystem-initiated, no user signature
Can execute during liquidation modeNormal user RFQs blocked when account is liquidating
Can use auto_lend_redeemOnly CollateralConversion system type has this privilege

How account is determined:

rust
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/)

FilePurpose
state/rfq-convert/atoms.tsState atoms (amounts, quote, RFQ ID)
state/rfq-convert/provider.tsxRFQ submission, WebSocket listener, debouncing
navigation/convert/ConvertScreen.tsxMain convert UI
navigation/convert/ConfirmConvertScreen.tsxConfirmation & execution
bottom-sheets/BottomSheetConvertSlippageAdjuster.tsxSlippage settings

Web App (bpx-web/src/components/modals/ConvertModal/)

FilePurpose
hooks/useConvertForm.tsxAll form logic, RFQ submission, WebSocket
screens/ConvertFormScreen.tsxMain input UI
screens/ConvertConfirmScreen.tsxConfirmation & execution
screens/ConvertSelectTokenScreen.tsxToken picker

Backend (bpx-backend/)

FilePurpose
core/types/src/models/rfq.rsRFQ & Quote data models
api/src/routes/rfq/request.rsSubmit/accept/cancel RFQ endpoints
api/src/routes/rfq/quote.rsSubmit quote endpoint
engine/src/engine/rfq/mod.rsRFQ matching logic, immediate execution
engine/src/clearing_house/clearing/rfq.rsSoft clear (20% buffer)
engine/src/clearing_house/settlement/rfq.rsBalance updates & fee deduction

Convert Limitations (vs Trading)

Convert does NOT enable autoBorrow - you can only swap what you actually have.

typescript
// What Convert sends:
await submitRfq({
  quantity,
  symbol,
  side,
  autoLendRedeem: true,   // ✅ Can redeem lends
  // autoBorrow: ???      // ❌ NOT sent (defaults to false)
});
FeatureConvertSpot 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:

  1. If one token is USDC → USDC is always the quote
  2. Otherwise → alphabetically sorted
typescript
// 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 ActionWhat's SentMeaning
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.

typescript
// 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.

typescript
// 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 → Bid

Examples (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:

typescript
clientIdRef.current = Math.floor(Math.random() * 2147483647);

await submitRfq({
  clientId: clientIdRef.current,
  // ...
});

When WebSocket events arrive, the app checks if the clientId matches:

typescript
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:

PlatformDebounce Time
Mobile500ms
Web1000ms
typescript
// 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:

  1. RFQ sits in "active" state
  2. User sees "Fetching quote..." indefinitely
  3. After 2 minutes, RFQ expires
  4. App auto-submits a new RFQ (if user is still on screen)
  5. 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

FeatureBackpackTypical RFQ
Quote formatTwo-sided (bid+ask)Single price
Auto-borrow/lendBuilt-inSeparate flow
Fee visibilityEmbedded in priceShown separately
Best quote trackingAuto-sorted BTreeSetManual comparison
Immediate executionAuto-accept if within limitAlways wait for user
Liquidation integrationCollateralConversion (atomic)Separate system
Quote on refreshAll cancelledVaries