RFQ (Request for Quotation) Workflow

Complete guide to the RFQ lifecycle. For quick SDK usage, see SDK Quick Reference.

This document explains the complete RFQ lifecycle in the Thetanuts options protocol - from user request to option settlement.

Table of Contents


Overview

The RFQ system is a sealed-bid auction mechanism for trading options. Key features:

  • Privacy: Offers are encrypted until reveal phase (prevents front-running)

  • Competitive Pricing: Market makers compete to provide best prices

  • Full Collateralization: All options are 100% collateralized

  • Atomic Settlement: Option creation and fund transfer happen atomically

Timeline


Reserve Price Explained

Reserve price is a critical RFQ parameter that protects you from unfavorable fills. It sets the maximum (for BUY) or minimum (for SELL) acceptable price per contract.

For BUY Positions (isLong: true)

When buying options:

  • Reserve price = Maximum you're willing to pay per contract

  • Total escrow = reservePrice × numContracts

  • If best MM offer exceeds your reserve price, the RFQ fails (funds returned)

  • Setting reservePrice: 0 means no price protection (accept any offer)

For SELL Positions (isLong: false)

When selling options:

  • Reserve price = Minimum you're willing to accept per contract

  • Acts as a floor price protection

  • If best MM offer is below your reserve price, the RFQ fails

  • Setting reservePrice: 0 means accept any offer (not recommended for sells)

Reserve Price Calculation Example

Parameter
Value

Reserve Price

0.015 (per contract)

Number of Contracts

10

Total Escrow Required

0.15 (in collateral token)

For a BUY position with USDC collateral:

  • Reserve price: 0.015 USDC per contract

  • Contracts: 10

  • Escrow: 0.015 × 10 = 0.15 USDC locked until settlement

When Reserve Price is Checked

  1. At Settlement: The winning offer is compared against reserve price

  2. BUY: If offerPrice > reservePrice → RFQ fails

  3. SELL: If offerPrice < reservePrice → RFQ fails

  4. Success: User pays/receives the actual offer price (not reserve price)


Option Structures

The RFQ system supports four option structures, determined by the number of strikes provided.

How the SDK Detects Structure

Settlement Types: All multi-leg structures (spreads, butterflies, condors) are cash-settled only. Physically settled options are available only for vanilla (single-strike) options. See Physically Settled Options for details.

Vanilla Options (Single Leg)

Spread Options (2 Legs)

Butterfly Options (3 Legs)

Condor Options (4 Legs)

RFQ Parameters Comparison

Parameter
Vanilla
Spread
Butterfly
Condor

strikes[]

1 value

2 values

3 values

4 values

isCall

true/false

true/false

true/false

true/false

numContracts

N

N

N

N

Collateral (PUT)

strike × N

(s2-s1) × N

(s2-s1) × N

(s2-s1) × N

Collateral (CALL)

N contracts

(s2-s1) × N

(s2-s1) × N

(s2-s1) × N

Max Loss

Full strike

Spread width

Wing width

Wing width

Creating Each Structure

Risk/Reward Comparison


Timeline


User Flow Diagram

Sequence of Events

Fund Flow

Summary Table

Step
Actor
Action
Result

1

User

Create RFQ

RFQ published on-chain

2

MM

See RFQ event

Calculate pricing

3

MM

Submit encrypted offer

Offer stored (hidden)

4a

User

Early settle (optional)

Accept offer immediately

4b

MM

Reveal offer

Amount becomes visible

4b

User

Normal settle

Accept best offer

5

Factory

Deploy option

New contract created

6

Factory

Transfer collateral

Locked in option

7

Factory

Transfer premium

Seller receives payment

8

User/MM

Claim at expiry

Payout distributed


Phase 1: User Creates RFQ

What the User Provides

Parameter
Description
Example

underlying

Asset (ETH or BTC)

'ETH'

optionType

CALL or PUT

'PUT'

strike

Strike price

1850

expiry

Expiry timestamp

1741334400

numContracts

Number of contracts

1.5

isLong

BUY (true) or SELL (false)

true

collateralToken

USDC, WETH, or cbBTC

'USDC'

offerDeadlineMinutes

How long MMs can respond

60

reservePrice

Max/min acceptable price (optional)

0.015

Critical: collateralAmount is ALWAYS 0

When creating an RFQ, the collateralAmount parameter must always be 0:

Why? Collateral is NOT locked at RFQ creation. It's pulled at settlement time from both parties.

For BUY Positions (isLong = true)

When buying an option:

  • User deposits reservePrice as escrow (maximum they'll pay)

  • No collateral needed (MM provides collateral as seller)

  • Factory holds deposit until settlement

For SELL Positions (isLong = false)

When selling an option:

  • User must approve collateral tokens for the OptionFactory

  • Approval amount calculation:

    • CALL (inverse): approval = numContracts (1:1 with underlying)

    • PUT: approval = strike * numContracts / 10^8

  • Collateral pulled at settlement, not at RFQ creation


Phase 2: Market Makers Respond

How MMs Process an RFQ

  1. Monitor Chain: MMs listen for QuotationRequested events

  2. Validate RFQ: Check implementation, collateral token, strikes, expiry

  3. Fetch Prices: Get bid/ask from exchanges (Deribit, etc.)

  4. Calculate Offer: Apply collateral cost and fees

  5. Sign Offer: Create EIP-712 signature

  6. Encrypt Details: Use ECDH to encrypt for requester only

  7. Submit On-Chain: Call makeOfferForQuotation()

Offer Calculation

Market makers calculate offers based on:

What's Stored On-Chain

During offer period, only the signature is stored:

The actual offer amount is:

  • Encrypted with ECDH (only requester can decrypt)

  • NOT stored on-chain (privacy)

  • Revealed in Phase 3


Phase 3: Reveal Phase

Timeline

Reveal Process

Market makers call revealOffer() with:

Winner Selection

The factory validates and selects the best offer:


Settlement Paths: Early vs Normal

There are two ways to settle an RFQ - early settlement or normal settlement.

Early Settlement

User decrypts an offer and accepts it immediately, before the offer period ends.

When to use: You see a good offer and want to lock it in immediately.

Requirements:

  • Must decrypt the offer yourself (get offerAmount + nonce)

  • Must call settleQuotationEarly with decrypted values

  • Can happen anytime during offer period

Code Example:

Normal Settlement

Wait for the reveal period, let MMs reveal their offers, then settle with the best offer.

When to use: Let all MMs compete, auction determines best price.

Requirements:

  • Wait until offer period ends

  • MM must reveal their offer on-chain

  • Call settleQuotation (no decryption needed)

Code Example:

Who Can Call Settlement?

Important: Settlement is NOT automatic. Someone must call settleQuotation() on-chain.

Settlement Type
Who Can Call
When

Early Settlement

Only the requester

During offer period

Normal Settlement

Anyone (permissionless)

After reveal period ends

Why is Normal Settlement Permissionless?

After the reveal period:

  • All offers are public on-chain (no longer encrypted)

  • The winning offer is deterministic (best price wins)

  • No special permissions needed to execute

Who Typically Settles?

In practice, MMs often settle RFQs they've won because:

  1. MM bots auto-reveal and auto-settle after deadlines

  2. MMs want to lock in their winning position immediately

  3. It's in the MM's interest to complete the trade quickly

Example from RFQ 781:

What if Nobody Settles?

If neither the requester nor MMs call settleQuotation():

  • The RFQ remains in "settleable" state indefinitely

  • Collateral approvals remain in place

  • Either party can settle at any time

  • There is no expiration on the settlement window (only the option itself expires)

Settlement Paths Comparison

Settlement Flow Diagram

SDK Settlement Methods

Action
Method
When
Who Can Call

Early settle

encodeSettleQuotationEarly()

During offer period

Requester only

Normal settle

encodeSettleQuotation()

After reveal period

Anyone

Decrypt offer

client.rfqKeys.decryptOffer()

For early settlement

Requester only

Cancel RFQ

encodeCancelQuotation()

Before settlement

Requester only


Phase 4: Settlement

New Option Creation

When existingOptionAddress == address(0):

  1. Clone Contract: Factory creates option using EIP-1167 minimal proxy

  2. Initialize: Set buyer, seller, strikes, expiry, collateral

  3. Transfer Collateral: Move collateral from factory to option contract

  4. Pay Premium: Transfer premium to seller (minus fees)

  5. Return Excess: Refund any unused deposit to requester

Existing Option Transfer

When existingOptionAddress != address(0):

  1. Validate State: Option not settled, parameters match

  2. Transfer Ownership: Change buyer/seller addresses

  3. Handle Collateral: Return collateral if applicable

  4. Exchange Premium: Transfer between parties

Fee Structure


Collateral Handling

Key Principle: SELLER Always Provides Collateral

In every RFQ trade, the seller (the party going short) provides collateral. This is true regardless of who created the RFQ:

RFQ Type
Who is Seller
Who Provides Collateral

BUY (isLong: true)

Market Maker

Market Maker

SELL (isLong: false)

User/Requester

User/Requester

Where Collateral is Stored

Collateral is NOT held by users, MMs, or the OptionFactory. It flows to a dedicated Option Contract:

  1. OptionFactory - Only routes collateral, never holds it

  2. Option Contract (BaseOption) - Holds collateral from settlement until expiry

  3. At Payout - Option Contract distributes based on settlement price

Collateral Lifecycle

Stage
Action
Where Funds Are

Pre-RFQ

Seller approves OptionFactory

In seller's wallet

Settlement

OptionFactory pulls collateral

Transferred to Option Contract

During Life

Option is active

Locked in Option Contract

At Expiry

Settlement triggered

Option Contract calculates payout

Payout

Distribution

Sent to buyer/seller based on outcome

Payout Distribution at Expiry

Scenario
Buyer Receives
Seller Receives

Option expires worthless (OTM)

0

Full collateral returned

Option expires ITM

Intrinsic value

Collateral minus payout

Option exercised maximally

Full collateral

0

Example: BUY PUT Option Flow

User wants to BUY a PUT (go long). MM becomes seller:

  1. User creates RFQ with isLong: true

  2. MM submits offer (encrypted)

  3. At settlement:

    • MM's collateral (strike × contracts) → Option Contract

    • User's premium → MM (minus fees)

  4. At expiry:

    • If PUT is ITM: Option Contract pays user

    • If PUT is OTM: Option Contract returns collateral to MM

Example: SELL PUT Option Flow

User wants to SELL a PUT (go short). User becomes seller:

  1. User creates RFQ with isLong: false

  2. User approves collateral for OptionFactory

  3. MM submits offer (encrypted)

  4. At settlement:

    • User's collateral (strike × contracts) → Option Contract

    • MM's premium → User (minus fees)

  5. At expiry:

    • If PUT is ITM: Option Contract pays MM

    • If PUT is OTM: Option Contract returns collateral to user

Collateral Amount by Option Type

Option Type
Collateral Formula
Example

CALL (inverse)

numContracts

1 ETH per contract

PUT

strike × numContracts / 10^8

$1850 × 1 = 1850 USDC

CALL SPREAD

(strikeHigh - strikeLow) × contracts

($2000 - $1800) × 1 = $200

PUT SPREAD

(strikeHigh - strikeLow) × contracts

($1900 - $1700) × 1 = $200

BUTTERFLY

(strikeMiddle - strikeLow) × contracts

Max loss width

CONDOR

(strike2 - strike1) × contracts

Inner spread width

Premium vs Collateral

These are two separate concepts:

Concept
Who Pays
When
Purpose

Premium

Buyer

At settlement

Price for the option

Collateral

Seller

At settlement

Backs the option payout

  • Premium is the cost to buy an option (like an insurance premium)

  • Collateral is the security deposit that ensures seller can pay if option is exercised

Collateral Cost (Opportunity Cost)

When market makers quote prices, they factor in the cost of locking up collateral. This is calculated using APR rates that represent the opportunity cost of capital.

Collateral Type
Symbol
APR Rate

Bitcoin

cbBTC

1%

Ethereum

WETH

4%

US Dollar

USDC

7%

Collateral Cost Formula:

Example: Selling a $2000 PUT with 3-month expiry, USDC collateral:

  • Collateral Amount: $2000

  • APR: 7% (USDC)

  • Time to Expiry: 0.25 years (3 months)

  • Collateral Cost: $2000 × 0.07 × 0.25 = $35.00

This cost is factored into MM pricing. For detailed pricing information, see MM Pricing Module - Collateral APR Rates.

Source of truth: These APR rates match the production mm_bot.py implementation (thetanuts_rfq/scripts/mm_bot.py:789-794).


Sealed-Bid Auction Mechanism

Why Sealed Bids?

Traditional auctions have problems:

  • Front-running: Bots see bids and outbid by $0.01

  • Sniping: Wait until last second to bid

  • Collusion: MMs can coordinate prices

Sealed-bid auction prevents these:

  • Offers encrypted until reveal

  • Only requester can decrypt (ECDH)

  • No one knows others' bids during offer period

ECDH Encryption Flow


Key Management

The RFQ system uses ECDH (Elliptic Curve Diffie-Hellman) key pairs for encrypting offers. Proper key management is essential for decrypting offers from market makers.

Storage Providers

Environment
Default Provider
Persistence
Location

Node.js

FileStorageProvider

✅ Persistent

.thetanuts-keys/ directory

Browser

LocalStorageProvider

✅ Persistent

Browser localStorage

Testing

MemoryStorageProvider

❌ Lost on exit

In-memory only

Key Persistence Example

Key Backup Warning

⚠️ CRITICAL: Back up your RFQ private keys! If you lose your private key, you cannot decrypt offers made to your public key. There is no recovery mechanism.

  • Node.js: Keys stored in .thetanuts-keys/ with secure permissions

  • Browser: Keys stored in localStorage (cleared if user clears browser data)

Custom Storage Provider

Implement KeyStorageProvider for custom storage (database, cloud, etc.):


Encryption Technical Details

ECDH Key Exchange

The SDK uses secp256k1 ECDH for secure key exchange:

Nonce Format

The nonce field in encrypted offers can be in two formats:

Source
Format
Example

MM Bot

16-char hex string

"987563ef5fde9655"

SDK

Decimal string

"391788778684598574"

The SDK automatically detects and handles both formats during decryption.

Why X-Coordinate (Not Hash)?

The SDK uses the raw x-coordinate as the AES key (not SHA256 hash) because:

  • MM bots use shared_secret[:32] (Python) which is the x-coordinate

  • Ethers.js computeSharedSecret() returns 0x04 + x (32) + y (32)

  • Using x-coordinate directly ensures compatibility


Decryption Troubleshooting

Common Issues

1. "KeyNotFoundError: RFQ key not found"

Cause: Private key not in storage (lost or different machine)

Solution:

  • Ensure you're using the same storage provider as when creating the RFQ

  • Check .thetanuts-keys/ directory exists (Node.js)

  • If key is lost, you cannot decrypt - create a new RFQ with a new key

2. "DecryptionError: Invalid ciphertext"

Cause: Using wrong private key or corrupted data

Solution:

  • Verify the public key in the RFQ matches your stored keypair

  • Check that encryptedOffer data is complete (not truncated)

3. "DecryptionError: Authentication failed"

Cause: Key mismatch or tampered data

Solution:

  • Ensure you're using the keypair from when you created the RFQ

  • AES-GCM authentication failure means the shared secret is wrong

Debugging Decryption

Key Mismatch Prevention

  1. Always use getOrCreateKeyPair() - automatically loads existing or creates new

  2. Don't regenerate keys between creating RFQ and decrypting offers

  3. Back up keys if running in production


SDK Usage Examples

Creating an RFQ (BUY Position)

Creating an RFQ (SELL Position)

Using buildRFQRequest (Complete Helper)

Creating a Spread RFQ (Complete Example)

Spreads use 2 strikes instead of 1. The SDK automatically:

  • Detects spread structure from strikes.length === 2

  • Sorts strikes correctly (PUT: descending, CALL: ascending)

  • Selects the correct implementation (PUT_SPREAD or CALL_SPREAD)

  • Calculates collateral as (upperStrike - lowerStrike) × numContracts

Spread vs Vanilla: Key Differences

Aspect
Vanilla
Spread

strikes param

[1800] (1 value)

[1800, 2000] (2 values)

Implementation

PUT or INVERSE_CALL

PUT_SPREAD or CALL_SPREAD

Collateral

strike × numContracts

(upper - lower) × numContracts

Max Loss

Full strike value

Spread width

Strike Order

N/A

PUT: descending, CALL: ascending

Strike Ordering (Important!)

The SDK automatically sorts strikes based on option type:

This is required by the smart contract. You can pass strikes in any order - the SDK handles sorting.

Multi-Leg Structures

The SDK supports 4 structures based on strike count:


Common Pitfalls

1. Setting collateralAmount != 0

2. Not Approving Tokens for SELL

3. Reserve Price in Wrong Decimals

4. Expiry Before Offer Deadline


Settlement & Cancellation

Early Settlement

Requesters can accept a specific offer before the offer period ends. This requires decrypting the offer first.

Normal Settlement

After the reveal phase, anyone can settle the RFQ with the winning offer:

Cancellation

Requester can cancel their RFQ at any time:

Market maker can cancel their offer:

Timing Constraints

Action
When

Make Offer

Before offerEndTimestamp

Cancel Offer

Before reveal period ends

Cancel RFQ

Any time (requester only)

Early Settlement

Before offerEndTimestamp

Reveal Offer

After offerEndTimestamp, before reveal period ends

Normal Settlement

After reveal period ends


Physically Settled Options (Vanilla Only)

Physical options involve actual delivery of the underlying asset rather than cash settlement. They are only available for vanilla (single-strike) options.

Physical Option Settlement Behavior

Option
Direction
Collateral
At ITM Expiry

Physical PUT

SELL

USDC

Receive WETH, pay strike in USDC

Physical CALL

SELL

WETH

Receive strike in USDC, deliver WETH

Creating a Physical PUT RFQ

Note: Physical options are vanilla-only. Multi-leg structures (spreads, butterflies, condors) are cash-settled only.


Closing Existing Positions

When closing an existing position via RFQ, precision is critical. The numContracts value must match exactly with the on-chain value. Floating-point arithmetic can introduce tiny errors that cause position closes to fail.

The Precision Problem

Solution: Use BigInt for Exact Precision

The SDK accepts numContracts as number | bigint | string:

Input Type
Behavior
Use Case

number

Converted using token decimals

New positions with human-readable values

bigint

Used directly, no conversion

Closing positions - exact on-chain value

string

Parsed as BigInt

API/JSON responses where value is a string

Position Closing Example

Key Points for Position Closing

  1. Always use BigInt for numContracts when closing positions

  2. Set existingOptionAddress to link the RFQ to the option being closed

  3. Opposite direction: Use isLong: true to close a short, isLong: false to close a long

  4. Expiry must match: Use the same expiry as the original option (must be 8:00 UTC for MM acceptance)

String Input (from API/JSON)

If numContracts comes as a string from an API response:


Summary

  1. User creates RFQ with option parameters and ECDH public key

  2. MMs respond with encrypted, signed offers during offer period

  3. MMs reveal actual amounts after offer period ends

  4. Best offer wins and settlement creates/transfers the option

  5. Collateral flows at settlement, not at RFQ creation

  6. Always use collateralAmount = 0 in RFQ params

  7. Early settlement is possible by decrypting and accepting a specific offer


See Also

Last updated