# Thetanuts V4

<figure><img src="/files/mAYvQH5KtNgICsdjJHY4" alt=""><figcaption></figcaption></figure>

### Overview

Thetanuts has always pushed the boundaries of what DeFi-native options can be. **V4 is a new foundation**: a builder-first, RFQ-powered options infrastructure that supports event-driven structures, custom payoffs, and institutional liquidity—while preserving a smooth user experience.

**What V4 unlocks**

* **Greater flexibility** — tailored quotes for dynamic, event-driven structures.
* **Better execution** — tighter spreads and more competitive pricing via open quoting.
* **Deeper liquidity** — sourced directly from market participants.

> V4 is **chain-agnostic**, **trustless**, and **composable**—built to deploy where the best communities and infrastructure exist.

***

### RFQ System

At the heart of V4 is a **Request-for-Quote (RFQ) engine**. Instead of relying on AMMs, liquidity providers deliver **direct quotes** that apps can request and users can execute on-chain.

**Why RFQ**

* **Customized structures** → more predictable outcomes and reduced slippage.
* **Open competition** → market-driven pricing, tighter spreads.
* **Liquidity depth** → direct access to active market participants.

**Seeding liquidity (the flywheel)**

* **Early support** — select builder teams are already testing V4 and simulating real-world environments.
* **Scaling up** — as the quoting environment matures, we connect app teams and early LPs with professional market makers, deepening liquidity and tightening spreads.
* **Future breadth** — broader support for additional structures (e.g., American options) as flow grows.

> The RFQ engine strengthens as more apps, makers, and strategies plug in—forming a **self-reinforcing liquidity flywheel**.

***

### Architecture Summary

**Chain-agnostic by design**

* Deployable across Ethereum L2s, modular stacks, and emerging high-performance chains.
* Prioritizes **low transaction costs**, **fast settlement**, and **vibrant DeFi ecosystems** for composability.

**Trustless & decentralized**

* **All settlements on-chain**, verifiable by anyone.
* **Quotes & execution via smart contracts**, minimizing intermediaries.
* **Fully collateralized** interactions: users keep control of funds and positions while trades are honored by counterparties.

**Built for builders**

* Clean **RFQ rails** that power custom strategies without rebuilding an options desk.
* Composable primitives that downstream apps can integrate through SDKs/APIs.
* A roadmap that expands instruments, oracles, and settlement adapters as the ecosystem grows.

***

### Vision

We’re building the **Theta System**—a modular options ecosystem where every product shares the same RFQ liquidity backbone and composes with the rest.

**What the Theta System enables**

* **Structured vaults**, **event-driven options**, **custom RFQ strategies**, and **institutional access points** that connect seamlessly.
* **Builder extensibility** — plug into RFQ to launch specialized vaults or trading products.
* **Unified flow for makers** — market makers tap a consolidated stream of options demand across products.

**The flywheel**

> More products → More flow → Deeper liquidity → Better pricing → Even more products.

***

You can access our website at: <http://thetanuts.finance/>


# SDK

## Looking for the SDK?

The Thetanuts SDK documentation lives in a separate section of our docs and can be easy to miss from the navigation.

You can access it directly here: <https://docs.thetanuts.finance/sdk>

You can also find it from the main docs navigation:

* open the top navigation dropdown
* click **SDK**

We recommend bookmarking the SDK page for easier access in the future.

### Inside the SDK docs

You'll find documentation for:

* getting started
* installation
* client setup
* RFQ workflow
* API reference
* validation and errors
* migration guidance


# Overview

## **Thetanuts V4 · OptionBook Integration Guide**

> ⚠️ **Important:** All examples are for reference and **audit has not been completed yet**. **Review, test, and verify** before production. Thetanuts is not responsible for issues arising from this code.

### What is OptionBook?

OptionBook is a **decentralized options order book on Base**. Makers quote off-chain; takers fill on-chain.

**Flow**

1. **Makers** create signed orders (spreads, butterflies, condors).
2. **Orders** are aggregated and published via API.
3. **Takers** (your users) call `fillOrder()` on the OptionBook contract.
4. **Options** are deployed as new contracts with collateral locked.
5. **Settlement** is processed daily by Thetanuts.

**Builder Key:** The `referrer` parameter in `fillOrder()` tags positions created via **your** platform—use it to display only “your” positions and track referral fees.

### What you’ll build

* Fetch orders from API (dev/test) or your own indexer (prod).
* Execute USDC-collateral trades with `fillOrder()`.
* Calculate payouts.
* Fetch/filter user positions by **your referrer**.

*This guide uses **USDC** for clarity. OptionBook supports other tokens (WETH and CBBTC).*


# Network and Products

### Base Mainnet (r10) — Current Deployment

```json
{
  "chain_id": 8453,
  "option_book": "0xd58b814C7Ce700f251722b5555e25aE0fa8169A1",
  "deployment_block": 36596854,

  "tokens": {
    "USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "WETH": "0x4200000000000000000000000000000000000006",
    "CBBTC": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf",
    "aBasUSDC": "0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB",
    "aBasWETH": "0xD4a0e0b9149BCee3C920d2E00b5dE09138fd8bb7",
    "aBascbBTC": "0xBdb9300b7CDE636d9cD4AFF00f6F009fFBBc8EE6"
  },

  "price_feeds": {
    "BTC": "0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F",
    "ETH": "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70"
  }
}
```

### Product Implementations

```json
{
  "implementations": {
    "SPREAD": {
      "0x2Db5aFA04aeE616157Beb53b96612947b3d13eE3": "CALL_SPREAD (2 strikes)",
      "0x571471B2f823cC6B5683FC99ac6781209BC85F55": "PUT_SPREAD (2 strikes)"
    },
    "BUTTERFLY": {
      "0xb727690FDD4Bb0ff74f2f0CC3E68297850A634c5": "CALL_FLYS (3 strikes)",
      "0x78b02119007F9EFc2297A9738b9a47A3bc3c2777": "PUT_FLYS (3 strikes)"
    },
    "CONDOR": {
      "0x7D3C622852d71B932D0903F973cafF45BCdBa4F1": "CALL_CONDOR (4 strikes)",
      "0x5cc960B56049b6f850730FacB4F3EB45417c7679": "PUT_CONDOR (4 strikes)",
      "0xb200253b68Fbf18f31D813AECEf97be3A6246b79": "IRON_CONDOR (4 strikes)"
    }
  }
}
```

**Rule of thumb (by `order.strikes.length`):**

* `2` → **Spread**
* `3` → **Butterfly**
* `4` → **Condor / Iron Condor**


# Fetching Orders

> 💡 **Production advice:** run **your own indexer** from chain events. Use our API for development/testing.

### Endpoint

```
GET https://round-snowflake-9c31.devops-118.workers.dev/
```

### Polling & Freshness

* **Refresh every \~30s** (aligns with Odette.fi / Flys.bet cadence).
* Fetch **fresh** orders right before trade execution.
* Orders can expire or be filled between polls.

```ts
const REFRESH_INTERVAL = 30_000;

async function fetchAndUpdateOrders() {
  const res = await fetch('https://round-snowflake-9c31.devops-118.workers.dev/');
  const json = await res.json();
  updateOrdersDisplay(json.data.orders);
  if (json.data.market_data) updateMarketPrices(json.data.market_data);
}

fetchAndUpdateOrders();
setInterval(fetchAndUpdateOrders, REFRESH_INTERVAL);
```

### Response Shape (abridged)

```json
{
  "data": {
    "orders": [
      {
        "order": {
          "maker": "0x...",
          "collateral": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
          "isCall": true,
          "priceFeed": "0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F",
          "implementation": "0xb727690FDD4Bb0ff74f2f0CC3E68297850A634c5",
          "strikes": [100000000000, 110000000000],
          "expiry": 1734336000,
          "price": "5000000",
          "maxCollateralUsable": "1000000000",
          "isLong": true,
          "orderExpiryTimestamp": 1734336000,
          "numContracts": "0",
          "extraOptionData": "0x"
        },
        "nonce": "12345",
        "signature": "0x...",
        "optionBookAddress": "0x1fcA1052F45A3271F12221D4D990BfED4EE7D0b1"
      }
    ],
    "market_data": { "BTC": 95000.50, "ETH": 3500.25 }
  }
}
```

### Common Filters

```ts
// by strike count
const spreads = orders.filter(o => o.order.strikes.length === 2);
const butterflies = orders.filter(o => o.order.strikes.length === 3);
const condors = orders.filter(o => o.order.strikes.length === 4);

// by asset
const BTC_FEED = '0x64c9...48F';
const btcOrders = orders.filter(o => o.order.priceFeed === BTC_FEED);

// by collateral (USDC)
const USDC = '0x8335...913';
const usdcOrders = orders.filter(o => o.order.collateral.toLowerCase() === USDC.toLowerCase());

// by implementation
const CALL_SPREAD = '0x2Db5...eE3';
const callSpreads = orders.filter(o => o.order.implementation.toLowerCase() === CALL_SPREAD.toLowerCase());
```

### Decimals

| Field                 | Decimals | Example        | Human       |
| --------------------- | -------- | -------------- | ----------- |
| `strikes[]`           | 8        | `100000000000` | `1000`      |
| `price`               | 8        | `5000000`      | `0.05 USDC` |
| `maxCollateralUsable` | 6        | `1000000`      | `1 USDC`    |

```ts
const strike = order.strikes[0] / 1e8;
const price = Number(order.price) / 1e8;
```


# Executing Trades

### rereqs

```ts
const OPTION_BOOK = '0xd58b814C7Ce700f251722b5555e25aE0fa8169A1';
const USDC        = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const YOUR_REFERRER = '0x...'; // unique address for your platform
```

### Why the `referrer` matters

* Tags positions created via **your** platform.
* Enables client-side filtering for portfolio views.
* Tracks referral fees in `OrderFilled`.

> **Always** pass your `YOUR_REFERRER` as the 3rd arg to `fillOrder()`.

### Minimal ABIs (v2)

```ts
const OPTION_BOOK_ABI = [
  {
    "inputs":[
      {"components":[
        {"internalType":"address","name":"maker","type":"address"},
        {"internalType":"uint256","name":"orderExpiryTimestamp","type":"uint256"},
        {"internalType":"address","name":"collateral","type":"address"},
        {"internalType":"bool","name":"isCall","type":"bool"},
        {"internalType":"address","name":"priceFeed","type":"address"},
        {"internalType":"address","name":"implementation","type":"address"},
        {"internalType":"bool","name":"isLong","type":"bool"},
        {"internalType":"uint256","name":"maxCollateralUsable","type":"uint256"},
        {"internalType":"uint256[]","name":"strikes","type":"uint256[]"},
        {"internalType":"uint256","name":"expiry","type":"uint256"},
        {"internalType":"uint256","name":"price","type":"uint256"},
        {"internalType":"uint256","name":"numContracts","type":"uint256"},
        {"internalType":"bytes","name":"extraOptionData","type":"bytes"}
      ],"internalType":"struct OptionBook.Order","name":"order","type":"tuple"},
      {"internalType":"bytes","name":"signature","type":"bytes"},
      {"internalType":"address","name":"referrer","type":"address"}
    ],
    "name":"fillOrder","outputs":[{"internalType":"address","name":"optionAddress","type":"address"}],
    "stateMutability":"nonpayable","type":"function"
  },
  {"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"address","name":"referrer","type":"address"}],"name":"fees","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
  {"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"claimFees","outputs":[],"stateMutability":"nonpayable","type":"function"}
];

const ERC20_ABI = [
  {"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},
  {"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},
  {"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"}
];
```

### Trade Flow (ethers v5-style)

```ts
async function buyOption(orderData, usdcAmount) {
  const signer = web3Provider.getSigner();
  const user = await signer.getAddress();
  const usdc = new ethers.Contract(USDC, ERC20_ABI, signer);
  const book = new ethers.Contract(OPTION_BOOK, OPTION_BOOK_ABI, signer);

  const required = ethers.utils.parseUnits(String(usdcAmount), 6);
  const bal = await usdc.balanceOf(user);
  if (bal.lt(required)) throw new Error(`Need ${usdcAmount} USDC`);

  const allowance = await usdc.allowance(user, OPTION_BOOK);
  if (allowance.lt(required)) {
    const tx = await usdc.approve(OPTION_BOOK, required);
    await tx.wait();
  }

  const pricePer = Number(orderData.order.price) / 1e8; // USDC/contract
  const contractsToBuy = usdcAmount / pricePer;
  const numContracts = ethers.BigNumber.from(
    Math.floor(contractsToBuy * 1e6).toString() // USDC-scale
  );

  // DO NOT mutate order fields
  const orderParams = {
    maker: orderData.order.maker,
    orderExpiryTimestamp: orderData.order.orderExpiryTimestamp,
    collateral: orderData.order.collateral,
    isCall: orderData.order.isCall,
    priceFeed: orderData.order.priceFeed,
    implementation: orderData.order.implementation,
    isLong: orderData.order.isLong,
    maxCollateralUsable: orderData.order.maxCollateralUsable,
    strikes: orderData.order.strikes,
    expiry: orderData.order.expiry,
    price: orderData.order.price,
    extraOptionData: orderData.order.extraOptionData || "0x",
    numContracts: numContracts.toString()
  };

  const tx = await book.fillOrder(orderParams, orderData.signature, YOUR_REFERRER);
  const receipt = await tx.wait();
  if (receipt.status !== 1) throw new Error('Transaction failed');

  return { txHash: tx.hash, receipt };
}
```

#### Notes

* **Never** modify order fields (signature will break).
* USDC is **6 decimals**: `parseUnits(x, 6)`.
* Round **down** contracts (`Math.floor(… * 1e6)`).


# Payouts and Pricing Intuition

### Max Payout (bounded structures)

```
Max Payout = Strike Width × Number of Contracts
```

#### Strike Width by Type

* **Spreads (2 strikes):**

  ```ts
  const [k1, k2] = order.strikes.map(s => s / 1e8);
  const width = Math.abs(k2 - k1);
  ```
* **Butterflies (3 strikes):**

  ```ts
  const [lo, mid, hi] = order.strikes.map(s => s / 1e8);
  const width = mid - lo; // symmetric
  ```
* **Condors (4 strikes):**

  ```ts
  const k = order.strikes.map(s => s / 1e8);
  const width = k[1] - k[0]; // simplified
  ```

### Payout at Settlement (simplified, untested)

```ts
function payoutAtPrice(order, numContracts, S) {
  const K = order.strikes.map(s => s / 1e8);
  const isCall = order.isCall;

  if (K.length === 2) { // spreads
    const [L, U] = K;
    if (isCall) {
      if (S <= L) return 0;
      if (S >= U) return (U - L) * numContracts;
      return (S - L) * numContracts;
    } else {
      if (S >= U) return 0;
      if (S <= L) return (U - L) * numContracts;
      return (U - S) * numContracts;
    }
  }

  if (K.length === 3) { // butterflies
    const [L, M, U] = K; const w = M - L;
    if (S <= L || S >= U) return 0;
    if (S === M) return w * numContracts;
    return (S < M)
      ? ((S - L) / w) * w * numContracts
      : ((U - S) / w) * w * numContracts;
  }

  if (K.length === 4) { // condors
    const [K1, K2, K3, K4] = K;
    const max = (K2 - K1) * numContracts; // simplified
    if (S <= K1 || S >= K4) return 0;
    if (S >= K2 && S <= K3) return max;
    if (S < K2) return ((S - K1) / (K2 - K1)) * max;
    return ((K4 - S) / (K4 - K3)) * max;
  }

  return 0;
}
```


# Positions and History

> **Production advice:** index on-chain events yourself. Use these endpoints for dev/test.

### Base URL

```
https://odette.fi/api
```

### After a Trade — Trigger Sync

```
POST/GET https://optionbook-indexer.thetanuts.finance/api/v1/update
```

* Call **immediately** after a trade.
* If `{ status: "skipped" }`, retry after \~10s.
* Wait \~15s after a successful update before fetching positions.

### Fetch User Positions (OPEN)

```
GET https://optionbook-indexer.thetanuts.finance/api/v1/user/{userAddress}/positions
```

#### Response (abridged)

```json
[
  {
    "address": "0x...",
    "status": "open",
    "buyer": "0x...",
    "seller": "0x...",
    "referrer": "0x...",         // filter by YOUR_REFERRER
    "createdBy": "0x...",        // OptionBook contract (do not filter on this)
    "entryTimestamp": 1734336000,
    "entryTxHash": "0x...",
    "entryPremium": "1000000",
    "entryFeePaid": "30000",
    "collateralToken": "0x8335...913",
    "collateralSymbol": "USDC",
    "collateralDecimals": 6,
    "underlyingAsset": "BTC",
    "priceFeed": "0x64c9...48F",
    "strikes": ["10000000000", "11000000000"],
    "expiryTimestamp": 1734364800,
    "numContracts": "20000000",
    "collateralAmount": "100000000",
    "optionType": 257,
    "settlement": null,
    "explicitClose": null
  }
]
```

#### Filter by **your** referrer

```ts
const MY_REFERRER = '0x...';
const mine = positions.filter(p => p.referrer.toLowerCase() === MY_REFERRER.toLowerCase());
```

### Fetch User History (SETTLED)

```
GET https://optionbook-indexer.thetanuts.finance/api/v1/user/{userAddress}/history
```

Same shape as positions, with:

* `status: "settled"`
* `settlement` populated (`settlementPrice` 8 dp, `payoutBuyer` 6 dp, etc.)

### Other Endpoints

```
GET https://optionbook-indexer.thetanuts.finance/api/v1/open-positions   // all open positions
GET https://optionbook-indexer.thetanuts.finance/api/v1/stats            // protocol stats
```


# Troubleshooting

#### “Signer Not Authorized”

**Cause:** stale order data.\
**Fix:** fetch fresh order data **right** before `fillOrder()`.

#### “Signature Mismatch”

**Cause:** you changed one or more order fields.\
**Fix:** pass API fields **verbatim** (incl. `isLong`, `strikes`, `price`, etc.).

#### “Insufficient Allowance”

**Cause:** USDC not approved.\
**Fix:** check `allowance(owner, OPTION_BOOK)`; call `approve()` if needed.

#### “Transfer Amount Exceeds Balance”

**Cause:** rounding or price changed.\
**Fix:** always **round down** contracts (`Math.floor(... * 1e6)`).

#### “Position Not Showing”

**Cause:** indexer not caught up.\
**Fix:** call `/update`, wait \~15s, then refetch `/user/{addr}/positions`.


# Quick References

### Decimals

* **USDC:** 6
* **Strikes:** 8
* **Prices:** 8
* **WETH:** 18 (not used if USDC-only)
* **CBBTC:** 8

### Referrer

* Use a **unique** address for your platform.
* Pass as the **3rd arg** to `fillOrder()` on **every** trade.
* Filter user portfolios by `position.referrer`.

### Endpoints

```
Orders:    https://round-snowflake-9c31.devops-118.workers.dev/
Trigger:   https://optionbook-indexer.thetanuts.finance/api/v1/update
Positions: https://optionbook-indexer.thetanuts.finance/api/v1/user/{address}/positions
History:   https://optionbook-indexer.thetanuts.finance/api/v1/user/{address}/history
Stats:     https://optionbook-indexer.thetanuts.finance/api/v1/stats
```

### Example Flow (condensed)

```ts
// 1) fetch
const { data } = await (await fetch('https://round-snowflake-9c31.devops-118.workers.dev/')).json();

// 2) filter
const BTC = '0x64c9...48F'; const USDC = '0x8335...913';
const spreads = data.orders.filter(o =>
  o.order.priceFeed === BTC &&
  o.order.collateral.toLowerCase() === USDC.toLowerCase() &&
  o.order.strikes.length === 2
);

// 3) buy $10
await buyOption(spreads[0], 10);

// 4) sync + wait
await fetch('https://optionbook-indexer.thetanuts.finance/api/v1/update');
await new Promise(r => setTimeout(r, 15_000));

// 5) fetch positions → filter by referrer
```


# Production Checklist

* [ ] **Run your own indexer** (don’t rely on public API for production).
* [ ] **Freshness**: refetch order **right before** filling; check `orderExpiryTimestamp`.
* [ ] **Slippage/Drift**: re-price UX; handle maker order exhaustion.
* [ ] **Decimals**: USDC 6dp; strikes/prices 8dp; rounding **down** contracts.
* [ ] **Referrer**: unique address; pass to every `fillOrder()`; portfolio filtering uses `referrer`.
* [ ] **Retries & Backoff** on `/update` and portfolio fetches.
* [ ] **Event Subscriptions**: watch `OrderFilled`, option deployments, and settlement events.
* [ ] **Error UX**: authorization, allowance, balance, expiry, signature mismatch.
* [ ] **Security**: validate inputs; never trust client-only calc for funds-moving ops.
* [ ] **Monitoring**: on-chain lag, API health, quote freshness, failure alerts.
* [ ] **Versioning**: pin ABIs & contract addresses; document migrations.


# Token Utility

### **Token Utility** <a href="#c21a" id="c21a"></a>

$NUTS is the governance token of the Thetanuts Finance ecosystem, and will be used to achieve the long-term objectives of the protocol — including decentralization, governance, and incentives.

Holders will enjoy the following token utilities:

* **veTokenomics & Gauge Voting**: $NUTS can be staked to become $veNUTS at a 1:1 ratio — with holders of $veNUTS being able to participate in gauge voting for every component within the Thetanuts Finance ecosystem.
* **Governance & Decentralization**: Holders of $veNUTS will be able to participate in governing Thetanuts Finance toward its long-term objectives.
* **Value Accrual**: Fees earned by Thetanuts Finance will be accrued to the Treasury, in which the Thetanuts Finance DAO will have control over once governance is live.
* **Boosted Emissions**: Holders can further lock their $veNUTS to receive up to 1.5x Boosted Emissions on their liquidity incentives on Thetanuts Finance.

$NUTS can be staked to become $veNUTS at a 1:1 ratio, which can be linearly vested to become liquid $NUTS subject to a 90-day vesting period. $veNUTS holders will enjoy the features of being a member of the Thetanuts Finance community.

$veNUTS can be locked up to 12 months for Boosted Emissions for providing liquidity on Thetanuts Finance, during which $veNUTS cannot be vested.

### Liquidity Incentives <a href="#c21a" id="c21a"></a>

$NUTS will be used to achieve the long-term objectives of the protocol — including incentivizing liquidity on the core components of Thetanuts Finance v3. To ensure that incentives are directed toward long-term members of the community, Thetanuts Finance referenced and received inspiration from Curve’s pioneering vote-escrow model.

Liquidity incentives on Thetanuts Finance will be distributed in $veNUTS at the end of every epoch on Ethereum Mainnet, with $veNUTS becoming immediately productive after being claimed. To qualify for $veNUTS incentives, a user must also maintain a threshold in dLP relative to their TVL on Thetanuts Finance v3 throughout the epoch.

Holders of $veNUTS can receive up to 1.5x Boosted Emissions on their liquidity incentives on Thetanuts Finance when they lock their $veNUTS. This Boost Value can be calculated below. Users can choose to lock $veNUTS beyond their $veNUTS Lockable Threshold, but the user’s Boost Value will be still based on the $veNUTS Lockable Threshold.

> **Boost Value (capped at 1.5x Boost)** = User Locked $veNUTS / $veNUTS Lockable Threshold \* time weight \* dLP weight;\
> where **$veNUTS Lockable Threshold** = User TVL on Thetanuts Finance / Total TVL on Thetanuts Finance \* Circulating Locked $veNUTS

Inspired by Curve, liquidity incentives are expected to be distributed over an extended period of time — with 99% of liquidity incentives being distributed after >65 years. Specifically, emissions are reduced by a factor of 2 ^ (1 / 512) each week similar to Curve.

### Dynamic Liquidity Provisioning <a href="#c21a" id="c21a"></a>

Liquidity pools on AMMs are essential for on-chain liquidity, enabling users to trade the native tokens of protocols (inc. $NUTS) in a permissionless manner. Users can provide liquidity to $NUTS pools on Uniswap v2 — thereby improving liquidity and depth for on-chain trading of $NUTS, while being eligible for fees on Uniswap v2 and $veNUTS emissions. Users can add $ETH or $NUTS on Ethereum Mainnet — which is then deposited into Uniswap v2’s $NUTS/$ETH liquidity pool in a 50/50 composition.


# Token Distribution

### **Token Distribution** <a href="#eef6" id="eef6"></a>

$NUTS (ERC-20) will have a maximum supply of 10,000,000,000 tokens, and will have an initial circulating supply of \~7.5%.

<figure><img src="https://miro.medium.com/v2/resize:fit:700/0*zNuiBYDORyFaKKgV" alt="" height="394" width="700"><figcaption></figcaption></figure>

The token distribution of $NUTS will be as follows:

* **Community**: 43.5%
* **Investors**: 11.9%
* **Team**: 20%
* **Treasury**: 20.1%
* **Airdrop Season 1**: 3.0%
* **Public Sale**: 1.0%
* **Initial Liquidity**: 0.5%

<figure><img src="/files/yGC6gHPI998QBNp6qmsp" alt=""><figcaption></figcaption></figure>


# Season 1 Airdrop

## Overview

The Season 1 Airdrop will see 3.0% of $NUTS distributed to the Thetanuts Finance community, of which users will receive $veNUTS specifically.

$veNUTS is subject to a 90-day vesting period. Users may initiate an Advanced Vesting with an exit penalty of 30 - 100%, of which this penalty will be sent to the Thetanuts Finance Treasury, in which the Thetanuts Finance DAO will have control over once governance is live.


# Security

Thetanuts Finance uses battle-tested contracts and has been audited by multiple leading security firms. All our prior audits can be found here:

<table><thead><tr><th width="206">Auditor</th><th>Audit Scope</th></tr></thead><tbody><tr><td><a href="https://consensys.io/diligence/audits/private/f2sb58y9uhrao9/">Consensys Diligence</a><br>(November 2023)</td><td>$NUTS Token</td></tr><tr><td><a href="https://drive.google.com/file/d/1X0zRpY7WgWjoEEEChIiJRdxFOXKpW-tL/">Halborn</a><br>(November 2023)</td><td>Thetanuts Finance v3</td></tr><tr><td><a href="https://github.com/peckshield/publications/blob/master/audit_reports/PeckShield-Audit-Report-Thetanuts-v1.0.pdf">Peckshield</a><br>(May 2022)</td><td>Basic Vaults, Stronghold, Degen Vaults, Wheel Vaults, $NUTS Token</td></tr><tr><td><a href="https://drive.google.com/file/d/1MS_2CmNzIpOoSfjogLRVPubZOC5zmpvd/view">Zokyo</a><br>(March 2022)</td><td>Basic Vaults, Stronghold, Degen Vaults, Wheel Vaults, $NUTS Token</td></tr><tr><td><a href="https://drive.google.com/file/d/1yOHxBlbZI_qoNgTRZeT-3g5syWP6w3sd/view">Akira Tech</a><br>(March 2022)</td><td>Basic Vaults, Stronghold, Degen Vaults</td></tr><tr><td><a href="https://drive.google.com/file/d/1mZoUMop8YeJRwZdC6dMSqBanBCtYasFm/view">Akira Tech</a><br>(February 2022)</td><td>Basic Vaults, Stronghold</td></tr><tr><td><a href="https://x41-dsec.de/defi/smartcontract/smart/contract/ethereum/eth/thetanuts/solidity/security/report/2021/12/14/thetanuts.finance-smart-contract-audit/">X41 D-Sec</a><br>(December 2021)</td><td>Basic Vaults</td></tr></tbody></table>


# Deployed Contracts

### $NUTS Governance Token

<table><thead><tr><th width="154">Chain</th><th>Address</th></tr></thead><tbody><tr><td><strong>Ethereum</strong></td><td><ul><li><strong>$NUTS</strong>:<br>0x23f3D4625AEF6f0b84d50dB1d53516e6015c0c9B</li><li><strong>$veNUTS</strong>:<br>0x0495F34E72189F8a77Ad47B2f747Ea0EcaFd313D</li><li><strong>LinearVesting</strong>:<br>0x85Ea78257fb2A190e916C2223c40c1eA4891bDAe</li></ul></td></tr></tbody></table>

### Basic Vault Contracts

<table><thead><tr><th width="154">Chain</th><th>Address</th></tr></thead><tbody><tr><td><strong>Ethereum</strong></td><td><ul><li><strong>$ETH Call - Weekly (Ethereum)</strong>: 0x9014f8E90423766343Ed4fe41668563526dF6715</li><li><strong>$ETH Put - Weekly (Ethereum)</strong>: 0x6d2Cdb589BE6037Df1AeA5dC433829aD5aF30013</li><li><strong>$WBTC Call - Weekly (Ethereum)</strong>: 0x60a4422B6B52aEF50647c67F29D6a7e6DAc3CCBC</li><li><strong>$WBTC Put - Weekly (Ethereum)</strong>: 0x3BA337F3167eA35910E6979D5BC3b0AeE60E7d59</li><li><strong>Wheel Vault (Ethereum)</strong>: 0x3567e2A6E161f3623307Aa4e59ceab6dEFf6291f</li></ul></td></tr><tr><td><strong>BNB Chain</strong></td><td><ul><li><strong>$ADA Call - Bi-weekly (BNB Chain)</strong>: 0xF98297A842f52Cd1f6c6f5f003Cd701813b1C461</li><li><strong>$ADA Put - Bi-weekly (BNB Chain)</strong>: 0x8BE731cB3b301b4a209C1A38ea14D6438e6913F6</li><li><strong>$BCH Call - Bi-weekly (BNB Chain)</strong>: 0xc879ecC0d2cdA26072e9049178a99B26C51eDF8a</li><li><strong>$BCH Put - Bi-weekly (BNB Chain)</strong>: 0xfe9B8054B947aCEeC01912Cf1811DB06fc804b69</li><li><strong>$BNB Call - Bi-weekly (BNB Chain)</strong>: 0x9EF72De1782431cf54518c42C06e26014E7201D1</li><li><strong>$BNB Put - Bi-weekly (BNB Chain)</strong>: 0xc75C3BE0Bc41857B9c1a675475F6E0a7c5Db63fC</li></ul></td></tr><tr><td><strong>Polygon</strong></td><td><ul><li><strong>$MATIC Call - Bi-weekly (Polygon)</strong>: 0x9dA79023Af00d1f2054BB1eED0D49004fe41C5b5</li><li><strong>$MATIC Put - Bi-weekly (Polygon)</strong>: 0x1724B8679A9CaD6CABDef7DbEE1d5b03b44584B2</li></ul></td></tr><tr><td><strong>Avalanche</strong></td><td><ul><li><strong>$AVAX Call - Bi-weekly (Avalanche)</strong>: 0xd06Bd68d58eD40CC2031238A3993b99172ea37cA</li><li><strong>$AVAX Put - Bi-weekly (Avalanche)</strong>: 0xa84aA41B6287aFE467ccE688f3796A2205198F07</li></ul></td></tr><tr><td><strong>Boba Network</strong></td><td><ul><li><strong>$BOBA Call - Bi-weekly (Boba Network)</strong>: 0x5a9f1D95C59365613B4224e690Bb4971DD246142</li></ul></td></tr><tr><td><strong>Cronos</strong></td><td><ul><li><strong>$CRO Call - Bi-weekly (Cronos)</strong>: 0x99F05418967d3596CAfd260913b682Fd9b0CBB40</li></ul></td></tr><tr><td><strong>Aurora</strong></td><td><ul><li><strong>$NEAR Call - Bi-weekly (Aurora)</strong>: 0xfc7F11Bb0d97d9db1f701eEA0fDE611536F1EB5F</li></ul></td></tr><tr><td><strong>Filecoin</strong></td><td><ul><li><strong>$FIL Call - Bi-weekly (Filecoin)</strong>: 0x03e35ed48101587F1A7B2C75f9A181D32BdF2D7D</li></ul></td></tr><tr><td><strong>Arbitrum</strong></td><td><ul><li><strong>$ARB Call - Bi-weekly (Arbitrum)</strong>: 0x0833EC3262Dcc417D88f85Ed5E1EBAf768080f41</li><li><strong>$ARB Put - Bi-weekly (Arbitrum)</strong>: 0xf94ea5B18401821BE07FBfF535B8211B061A7F70</li><li><strong>$ETH Call - Weekly (Arbitrum)</strong>: 0x1D1CD4abe0F2AF9d79b5e3149BF4A503f97C1EAd</li><li><strong>$ETH Put - Weekly (Arbitrum)</strong>: 0xA8459eC6DF0D9a61058C43a308dD8A2CEc9d550E</li></ul></td></tr><tr><td><strong>Polygon ZkEVM</strong></td><td><ul><li><strong>$stMATIC Call - Bi-weekly (Polygon ZkEVM)</strong>: 0x7bF3c7C23501EA3E09B237D6F8AdcB7Ea3CeF41C</li></ul></td></tr><tr><td>Mantle</td><td><ul><li><strong>$mETH Call (ETH) - Bi-weekly (Mantle):</strong> 0x7bF3c7C23501EA3E09B237D6F8AdcB7Ea3CeF41C</li><li><strong>$fBTC Call (BTC) - Bi-weekly (Mantle)</strong>: 0x40cAbD573Bd1cc1D499353a75DD15EB9a961c038</li><li><strong>$ETH Put - Bi-weekly (Mantle)</strong> 0x8C21d69dC3EbcB9180DBAf8a81c7568884785710</li><li><strong>$BTC Put - Bi-weekly (Mantle)</strong>: 0xaCc3Ae01a38962351a25EA29B324FD2323DAa8C6</li></ul></td></tr></tbody></table>

### Thetanuts Finance v3 Contracts

<table><thead><tr><th width="155">Contract</th><th>Address</th></tr></thead><tbody><tr><td><strong>Position Managers</strong><br><strong>(Arbitrum)</strong></td><td><ul><li><strong>PositionManager</strong>: <br>0xE65974aeC8701135b8e34322942a2A25b0561161</li><li><strong>SubPositionManager</strong>: 0xF96Bf7377Bd4f95e709b96a24d36DDC01681b18a</li><li><strong>DepositQueue</strong>: <br>0xbaA264007B4461dF831ea83D4002335Faa428469</li></ul></td></tr><tr><td><strong>ARB-C ARB-BI-C-10Delta (Arbitrum)</strong></td><td><ul><li><strong>ARB-C Lending Pool</strong>: 0xB1D961701be27d26A2E4FA13cb0c4FA7C47DD615</li><li><strong>ARB-C Liquidity Vault</strong>: 0x2F2814dA81716B7FCD8AF15c3E81db2186DC7239</li><li><strong>ARB-C LendingPoolAddressesProvider</strong>: 0x9502279858df9b89fd1900ee4B921a5DD62a096c</li><li><strong>ARB-C AaveProtocolDataProvider</strong>: 0x1a0D579B15045e668Fc995989d399784c9EccaeD</li><li><strong>ARB-C V3 Pool</strong>: <br>0xe0cD3746C8dEe433E064074A1d9193806EC63D16</li></ul></td></tr><tr><td><strong>ARB-P ARB-Bi-P-10Delta (Arbitrum)</strong></td><td><ul><li><strong>ARB-P Lending Pool</strong>: 0xeb9a387494922d5Ec20631c19D51c39C45AC8643</li><li><strong>ARB-P Liquidity Vault</strong>: 0x85ceD6055f0eC21dE0e4216177115cbc6a930ACa</li><li><strong>ARB-P LendingPoolAddressesProvider</strong>: 0x3938a38E0e569EDBaB089Db1E2393546b14EcED8</li><li><strong>ARB-P AaveProtocolDataProvider</strong>: 0x2b99942ae31e02a6C48A32A974449B1546f2Cbc5</li><li><strong>ARB-P V3 Pool</strong>: <br>0x4cD207D3BB951DFfEE3fae27A0bE70019Bb72666</li></ul></td></tr><tr><td><strong>ETH-C WETH-Wk-C-10Delta (Arbitrum)</strong></td><td><ul><li><strong>ETH-C Lending Pool</strong>: 0xF9a82c9Dd095076FCdc7507D6cf920EEfFa93BA2</li><li><strong>ETH-C LendingPoolAddressesProvider</strong>: 0x552c7494441132860C5D1a9594B386A0391Cf39B</li><li><strong>ETH-C AaveProtocolDataProvider</strong>: 0x552c7494441132860C5D1a9594B386A0391Cf39B</li></ul></td></tr><tr><td><strong>Configuration (Arbitrum)</strong></td><td><ul><li><strong>LendingPoolAddressesProviderRegistry</strong>: 0xB9eD53B8632534f8bEcb0503C34Cf65b7957Bf18</li><li><strong>LendingPoolCollateralManager</strong>: 0xC0c69D17f3821Ef7423364a942677f91434F1d19</li></ul></td></tr><tr><td><strong>Tokenization (Arbitrum)</strong></td><td><ul><li><strong>ATokensAndRatesHelper</strong>: 0xF7779BdF50B5FeED804DBe79e3359772B1117b6D</li><li><strong>StableAndVariableTokensHelper</strong>: 0xF55594A9f5f83719b920bdbc49f2c1c83a1aCb97</li><li><strong>AToken</strong>: <br>0xC66F6F50f3e40B93A99F5471b3FfEDf40D886D3d</li><li><strong>VariableDebtToken</strong>: <br>0x2ff3E2a4eb81e93091Ad3744B5791F201f06DD76</li><li><strong>StableDebtToken</strong>: <br>0x8954957456750AC7743D889f1e39Ea0CbfeAE1A3</li></ul></td></tr><tr><td><strong>Misc. (Arbitrum)</strong></td><td><ul><li><strong>ReserveLogic</strong>: <br>0x266C10F2B5B95955e459659b600B2f5A23707083</li><li><strong>GenericLogic</strong>: <br>0xe50d41E66A70D087F5480Ef84ABcF2e312534b05</li><li><strong>ValidationLogic</strong>: <br>0x59ebd965F0020a8fcd81B5B51a35DE974205d02A</li><li><strong>LendingPool</strong>: <br>0xB8656F97F98b7edc47a966790FF468539Cec1617</li><li><strong>LendingPoolConfigurator</strong>: 0x5a9f1D95C59365613B4224e690Bb4971DD246142</li><li><strong>WETHGateway</strong>: <br>0x55bc77bc0F19056BE5aEb09f4FF5298A923eFf56</li><li><strong>LendingRateOracle</strong>: 0x966c80906C5d241c6Be87a9e3c9Fc6Ca89E88427</li><li><strong>AaveOracle</strong>: <br>0x9D4a7Fa9D8D6eDb5C4f33564E886191072779c3f</li><li><strong>WalletBalanceProvider</strong>: 0x5376016f5aC922e989253054fbCEef2E8796beB0</li><li><strong>DefaultReserveInterestRateStrategy</strong>: 0x93aaFa036B6BACFc5DACffCA9aFdff5087237e52</li></ul></td></tr></tbody></table>


# FAQ

### 🌰 Overview

<details>

<summary>What is Thetanuts Finance?</summary>

Thetanuts Finance is a decentralized on-chain options protocol focused on altcoin options, allowing users to go long or short on on-chain options.

The first iteration of Thetanuts Finance in September 2021 saw the launch of Basic Vaults — where the Basic Vaults sold out-of-money (“OTM”) European cash-settled options to accredited market makers, and generate yields for users in the form of option premiums.

With the v3 upgrade, Thetanuts Finance will pivot toward a decentralized on-chain options protocol focused on altcoin options. Our first-to-market v3 architecture uses our existing Basic Vault LP Tokens to extend new use cases in on-chain options trading — enabled by a Lending Market and Uniswap v3 Pools, which are all housed within the v3 interface.

</details>

<details>

<summary>What is the rationale behind the Thetanuts Finance v3 Upgrade?</summary>

Option Vault protocols performed well in 2021/2022 for a number of reasons, hitting a peak TVL of $550m in April 2022 - and currently down \~90% from April 2022 highs.

As Option Vaults became a mature product, market participants have identified a number of structural shortcomings with Option Vault mechanisms — which eventually led to their decline.

Despite the structural shortcomings and decline of the Option Vault sector within DeFi — Thetanuts Finance was the second largest Option Vault protocol by TVL, and the Options protocol with the largest coverage of altcoin options.

With these successes in mind and in wanting to leverage our track record of being a leading Options protocol focused on altcoin options, Thetanuts Finance v3 will see us move away from being an Option Vault protocol and becoming a decentralised on-chain options protocol focused on altcoins.

We outline the benefits of our first-to-market v3 architecture [here](#overview).

</details>

<details>

<summary>What are the benefits of the Thetanuts Finance v3 Upgrade?</summary>

* **Basic Vaults Remain the Core of Thetanuts Finance v3**: Basic Vaults will continue to be at the core of Thetanuts Finance, with the Basic Vault LP tokens being used to bootstrap the rest of the v3 ecosystem. This allows Thetanuts Finance to leverage their prior track record of leading Option Vault protocol focused on altcoins.
* **Housing v3 under a Unified Interface**: v3 will feature an upgraded trading-like interface, abstracting the need for traders to interact individually with Basic Vaults, AMM, and the Lending / Borrowing Market.
* **Ability to Buy Options**: Option Vault strategies mainly center around the sell-side, with no ability for users to go long — v3 will change this through the use of the AMM and Lending Market.
* **Flexibility to Exit Basic Vault LP Positions**: Prior to v3, Vault LPs would need to wait until the end of the next epoch — implying that they could not exit their positions until a week later. With the AMM, a user could simply swap out of the Basic Vault LP token — instantly closing their position without having to wait for the end of the epoch.
* **Battle-tested Infrastructure:** Aave v2 was referenced during the creation of the Thetanuts Finance v3 Lending Market, with the protocol never being exploited prior. Combined with the Thetanuts Finance and Uniswap v3 tech stack that has never been exploited, the Thetanuts Finance team is confident of the security and safety of the v3 upgrade.
* **Additional Sources of Yield and Incentives:** Through Thetanuts Finance v3, users have access to additional sources of yield beyond just option premiums from the Basic Vaults. These additional sources include: lending interest from the Lending Market, trading fees from AMM, potential $NUTS token incentives, and potential additional token incentives.
* **Protocol Solvency**: Basic Vaults in Thetanuts Finance are 100% collateralized.
* **Free Market Pricing:** Unlike other option protocols where IV is used as an input to price options, v3 features free market pricing — whereby if users believe that AMM pricing is incorrect, they can arbitrage this opportunity for profit.

</details>

<details>

<summary>Which networks does Thetanuts Finance support?</summary>

We currently support Ethereum, BNB Chain, Polygon, Avalanche, Cronos, Arbitrum, Filecoin, and Polygon ZkEVM. We intend to support additional networks in the future.

</details>

<details>

<summary>Are there incentives associated with providing liquidity on Thetanuts Finance?</summary>

Users will have access to up to five sources of yield on Thetanuts Finance v3.

These five sources include:\
\- Option Premiums from Basic Vaults\
\- Lending Interest from Lending Market\
\- Trading Fees from AMM\
\- Potential $NUTS Token Incentives (Future)\
\- Potential Additional Token Incentives (Future)\
\
For more information, please see [here](broken://pages/ctSRyE2OOD7F02vcROC2).

</details>

<details>

<summary>Are there fees associated with using Thetanuts Finance?</summary>

Users can expect to see a number of fees on Thetanuts Finance, corresponding to the different modules on Thetanuts Finance v3.

Protocol fees earned by Thetanuts Finance will be accrued to the Treasury, in which the Thetanuts Finance DAO will have governance rights over.

For more information, please see [here](broken://pages/PRAVXjmPlaxuK76gYJLa).

</details>

<details>

<summary>Are options on Thetanuts Finance cash or physically settled?</summary>

Options on Thetanuts Finance v3 are cash-settled - involving the delivery of the net cash amount on the settlement date if the option contract is exercised, as opposed to delivery of the underlying asset in a physically-settled option contract.

</details>

<details>

<summary>Are options on Thetanuts Finance American or European?</summary>

Options on Thetanuts Finance v3 are European-styled, meaning that they may only be exercised at the expiration date of the option, i.e. at a single pre-defined point in time. At expiration, the Basic Vault option contracts will be exercised if they are in-the-money ("ITM").

</details>

### ⚙️ Mechanics

<details>

<summary>What happens under the hood when a long or short position is executed?</summary>

A short volatility position on Thetanuts Finance represents holding of a Basic Vault LP Token.\
\- **Short Call**: $XYZ swapped for $XYZ-C on the AMM, with $XYZ-C representing a short call.\
\- **Short Put**: $USDC swapped for $XYZ-P on the AMM, with $XYZ-P representing a short put.

Meanwhile, a long volatility position on Thetanuts Finance sees a number of steps within the v3 architecture in order to achieve long exposure.\
\
**Long Call**\
$XYZ collateral deposited into the v3 Lending Market. A flash loan for $XYZ-C occurs at a 95% LTV on the Lending Market, with up to 20x of collateral value borrowed. Borrowed $XYZ-C is sold for $XYZ in the AMM, achieving a long call position.

**Long Put**\
$USDC collateral is supplied into the v3 Lending Market. A flash loan of $XYZ-P occurs at a 95% LTV on the Lending Market, with up to 20x of collateral value borrowed. Borrowed $XYZ-P is sold for $USDC in the AMM, achieving a long put position.

</details>

<details>

<summary>What happens under the hood when a position is Closed Now?</summary>

Users on Thetanuts Finance v3 can opt to Close Now on their open positions, which has the effect of closing their positions instantly.&#x20;

**Short Call**\
Swap $XYZ-C to $XYZ on the AMM. If there is insufficient liquidity in the AMM to facilitate a Close Now, the user can instead close the position Upon Expiry instead.

**Short Put**\
Swap $XYZ-P to $USDC on the AMM. If there is insufficient liquidity in the AMM to facilitate a Close Now, the user can instead close the position Upon Expiry instead.

**Long Call**\
Thetanuts Finance v3 will route the order to the venue with the best pricing, by querying both the Basic Vaults (for Mint Price) and the AMM (for AMM Price).

* If AMM Price is more favourable, $XYZ is sold in the AMM for $XYZ-C, with the $XYZ-C loan then being returned to the Lending market – thereafter, the initial $XYZ position is then withdrawn from the Lending market.
* If Mint Price is more favourable, $XYZ will be deposited into the Basic Vault to mint $XYZ-C, with the loan being returned and $XYZ collateral in the Lending market redeemed.
* $XYZ in excess of loan repaid represents P\&L.

**Long Put**\
Thetanuts Finance v3 will route the order to the venue with the best pricing, by querying both the Basic Vaults (for Mint Price) and the AMM (for AMM Price).

* If AMM Price is more favourable, $USDC is sold in the AMM for $XYZ-P, with the $XYZ-P loan then being returned to the Lending market – thereafter, the initial $USDC position is then withdrawn from the Lending market.
* If Mint Price is more favourable, $USDC will be deposited into the Basic Vault to mint $XYZ-P, with the loan being returned and $USDC collateral in the Lending market redeemed.
* $USDC in excess of loan repaid represents P\&L.

</details>

<details>

<summary>What happens under the hood when a position is Closed Upon Expiry?</summary>

Users on Thetanuts Finance v3 can opt to Close Upon Expiry their open positions - which closes the position at the end of the epoch.

**Long Call**\
At the end of the epoch, $XYZ position is deposited into the Basic Vault to generate $XYZ-C – which is used to repay any outstanding debt in the Lending Market. $XYZ in excess of loan repaid represents P\&L.

**Long Put**\
At the end of the epoch, $USDC position is deposited into the Basic Vault to generate $XYZ-P – which is used to repay any outstanding debt in the Lending Market. $USDC in excess of loan repaid represents P\&L.\
\
**Short Call**\
The user closes the Short Call (i.e. $XYZ-C) at the end of the Basic Vault epoch, and claims the collateral after the epoch ends.\
\
**Short Put**\
The user closes the Short Put (i.e. $XYZ-P) at the end of the Basic Vault epoch, and claims the collateral after the epoch ends.

</details>

<details>

<summary>What is Position Health?</summary>

Long positions on Thetanuts Finance v3 have a "Position Health", referring to the collateralization of a position after paying Borrowing Interest to the Lending Market after every epoch.

Positions that have 0% Health will be closed at the end of every epoch by Thetanuts Finance in order to maintain solvency of the Lending Market.

To avoid closure of a long position, a user has the choice to "top-up" collateral to maintain their long position.

</details>

<details>

<summary>What are some considerations for opening positions on Thetanuts Finance v3?</summary>

* **Theoretical Leverage**: While the theoretical leverage based on a 95% LTV is 20x, leverage on Thetanuts Finance v3 is typically in the range of 15–20x — as a result of slippage, price impact, and fees on the AMM.
* **Managing Borrowing Costs**: Leveraging in this manner carries inherent risks. The triggers for liquidation on Thetanuts Finance v3 are the accumulating costs from borrowing (i.e. option premiums), rather than market volatility. This is possible given that value of the borrowed asset (i.e. XYZ-C or XYZ-P) can increase due to option premiums and interest payments, each lending position utilizes the underlying asset as collateral.
* **Automatic Rollover**: Positions not closed at the end of the epoch will automatically roll over to the new epoch, with any P\&L captured in the user’s net position in the lending market.
* **Protocol Solvency**: Basic Vaults in Thetanuts Finance are 100% collateralized.

</details>

<details>

<summary>Are there arbitrage opportunities on Thetanuts Finance v3?</summary>

The same Basic Vault LP Token will have both a Mint Price and an AMM Price, which may result in an arbitrage opportunity if they do not trade at the same level. This scenario occurs when the Mint Price > AMM Price for a Basic Vault LP Token, where a user could arbitrage this by:

* **For Calls:** Deposit $XYZ in the Basic Vault to mint $XYZ-C; with exposure on Thetanuts Finance v3 achieved by going short call on XYZ. Sell $XYZ-C directly on the AMM, receiving $XYZ.
* **For Puts:** Deposit $USDC in the Basic Vault to mint $XYZ-P; with exposure on Thetanuts Finance v3 achieved by going short put on XYZ. Sell $XYZ-P directly on the AMM, receiving $USDC.

</details>

### 🔒 Basic Vaults

<details>

<summary>What are Basic Vaults?</summary>

Basic Vaults sell out-of-money (“OTM”) European cash-settled options to accredited market makers, and generate yields for users in the form of option premiums. Vaults have pre-defined parameters (inc. strike price, delta, and tenor) – creating specific risk-adjusted yields on each vault, while also abstracting the complexity in users needing to design these parameters on their own.

Basic Vault LP Tokens represent short volatility instruments, either as short call or short put positions:\
\- **Selling covered calls (Short Call)**: Designed to generate income for users.\
\- **Selling cash-secured puts (Short Put)**: Designed to accumulate the underlying asset for users.

For more information, please see [here](#basic-vaults).

</details>

<details>

<summary>Who are the actors in the Basic Vaults?</summary>

There are two actors in the Basic Vaults.

**Liquidity Providers**\
Liquidity providers deposit collateral into the Basic Vaults, which in turn sells option contracts to market makers. In return, liquidity providers receive option premiums from market makers.&#x20;

**Market Makers**\
Whitelisted accredited market makers that participate in the blind auctions and buy option contracts from the Basic Vaults. In return, market makers pay option premiums to Liquidity Providers.

</details>

<details>

<summary>How do the Basic Vault blind auctions work?</summary>

Every Friday, Thetanuts Finance conducts blind auctions for the Basic Vaults off-chain - where whitelisted accredited market makers bid to buy option contracts sold by the Basic Vaults. The winning market maker of each Basic Vault then proceeds to deposit the option premiums upfront into the Basic Vault.

Upon expiry of the contract,  Thetanuts Finance references major centralised exchanges as a data source to determine settlement price - including Deribit Index (i.e. for majors), Binance Spot (i.e. for altcoins), and others (i.e. for longer tail assets). This settlement price determines whether the option contract has been struck. The Basic Vault would then settle with the winning market maker of that epoch, depending on whether the option contract is struck.

</details>

<details>

<summary>How are Basic Vaults utilized in Thetanuts Finance v3?</summary>

$XYZ-C (Call Basic Vault LP Token) and $XYZ-P (Put Basic Vault LP Token) represent fungible and transferrable tokenized Basic Vault positions – and form the backbone of all trading strategies on Thetanuts Finance v3.

Users that have deposited into the Basic Vaults will mint a Basic Vault LP Token, and can redeem the Basic Vault LP Token by initiating a withdrawal of collateral from their Basic Vault position. This Mint / Redeem mechanism also gives rise to a “**Mint Price**”, defined as the price of a Basic Vault LP Token when minted or redeemed from a Basic Vault.

</details>

<details>

<summary>What can Basic Vault LP Tokens be used for in Thetanuts Finance v3?</summary>

Basic Vault LP Tokens may be used to:

* **Provide Liquidity on Lending Market**: Basic Vault LP tokens can be deposited into the Lending Market to generate additional lending interest.
* **Provide Liquidity on AMM**: Basic Vault LP tokens can be deposited into the AMM to generate additional AMM trading fees.
* **Go Long**: Users interested in a long volatility position may borrow the Basic Vault LP Tokens from the Lending Market and sell them on the AMM, representing a long call or long put position.
* **Close Basic Vault Position Instantly**: A Basic Vault LP Token holder may sell the position on the AMM, representing an instant close of the short call or short put position without having to wait for the end of the Basic Vault epoch.

</details>

<details>

<summary>When can a user deposit into the Basic Vaults?</summary>

Users can deposit into the Basic Vaults via a short position on Thetanuts Finance v3 at any point in time – except when the Basic Vaults are in the Auction in Progress phase, where the Basic Vaults are locked to prevent any additional deposits or withdrawals.

Users that deposit into the Basic Vaults in the middle of an ongoing epoch will not be eligible for option premiums for that epoch, while also assuming the risk that the Basic Vault will be struck – where the user will lose a part of their deposited collateral.

</details>

<details>

<summary>When can I close my Basic Vault position?</summary>

Users can close their Basic Vault position (i.e. a short position on Thetanuts Finance v3) at any point in time. Users are given two choices in how they can close their position:\
\- **Close Now**: Basic Vault position closed instantly via the AMM.\
\- **Close Upon Expiry**: Basic Vault position closed at the end of the epoch.

If a user chooses to Close Upon Expiry, the Basic Vault collateral will have a “Withdrawal Pending” status – where users can cancel the Close Upon Expiry transaction at any time before the epoch has ended. When Basic Vault collateral has the “Withdrawal Pending” status, the collateral will be deposited into a withdrawal pool and will not participate in the next epoch. At the end of the epoch, the user can “Claim” the collateral from the withdrawal pool at any time.

</details>

<details>

<summary>Why can't I close my positions during the Auction in Progress phase?</summary>

The Basic Vaults are locked during the Auction in Progress phase, preventing users from depositing or withdrawing collateral. This is necessary for accredited market makers to determine the size of the options contract that they will bid on.

</details>

### 🏛 Lending Market

<details>

<summary>What is the Lending Market on Thetanuts Finance v3?</summary>

The v3 Lending Market will be housed within the v3 interface, with contracts being inspired by Aave v2. The Lending Market is a core module within Thetanuts Finance v3, enabling users to go long on on-chain options.

The Lending Market will enable flash loans to allow users to go long on on-chain options, borrowing up to 95% LTV and implying a theoretical 20x levreage limit on long positions. In reality - the leverage on a long position may be lower due to slippage, price impact, and fees on the AMM.

For more information, please see [here](#lending-market).

</details>

<details>

<summary>What are the supported assets on the Lending Market?</summary>

The Lending Market will support $XYZ-C, $XYZ-P, $XYZ, and $USDC assets.

</details>

<details>

<summary>What interest rate model does the Lending Market use?</summary>

The Lending Market will use a fixed interest rate model between epochs, which can be changed in the future based on governance.

</details>

<details>

<summary>Who are the actors in the Lending Market?</summary>

There are two actors in the Lending Market.

* **Lenders**: Lenders can deposit Basic Vault LP Tokens (i.e. $XYZ-C, $XYZ-P) or Collateral Assets (i.e. $XYZ, $USDC) into the Lending Market, and receive interest from Borrowers.
* **Borrowers**: Borrowers can borrow Basic Vault LP Tokens from the Lending Market, and sell them on the AMM - achieving a long call or long put position.

</details>

<details>

<summary>How do I provide liquidity to the Lending Market?</summary>

After a user opens a short call or short put position, the user will be prompted to “Boost” – which deposits the Basic Vault LP Tokens into the Lending Market. By providing liquidity on the Lending Market, Lenders generate yields in the form of Lending Interest. The Lending Market will use a fixed interest rate model between epochs, which can be changed in the future based on governance.

In the future, Thetanuts Finance v3 will enable users to deposit $XYZ and $USDC into the Lending Market for users to generate Lending Interest as well.

</details>

<details>

<summary>Can I borrow against my Lending Market position?</summary>

Lenders on the Lending Market may then borrow against their positions, borrowing $XYZ or $USDC depending on the pools that they have supplied. This enables users to achieve greater capital efficiency on their short position trading collateral, and allowing them to use borrowed capital from the Lending Market to interact with other components of the Thetanuts Finance v3 ecosystem.

</details>

### 💧 AMM

<details>

<summary>What is the AMM on Thetanuts Finance v3?</summary>

Thetanuts Finance v3 will utilize battle-tested Uniswap v3 pools to enable users to go long or short on on-chain options, and will form a key part Thetanuts Finance v3.

Uniswap v3 liquidity pool pairs will be denominated as $XYZ/$XYZ-C for Calls, and $XYZ-P/$USDC for Puts. These pools also gives rise to an “**AMM Price**”, defined as the price of a Basic Vault LP Token as set by the relative concentration of assets within the liquidity pool given by the x\*y=k.

</details>

<details>

<summary>Who are the actors in the AMM?</summary>

There are two actors in the AMM.

* **Liquidity Providers**: Liquidity Providers provide TVL, enabling Traders to go long or short on on-chain options. Liquidity Providers receive a share of AMM Trading Fees.
* **Traders**: With these Uniswap v3 liquidity pools, Traders can take on long volatility or short volatility positions.

</details>

<details>

<summary>How do I provide liquidity to the AMM?</summary>

To Add Liquidity into the AMM, a Liquidity Provider would deposit $XYZ (for Calls) or $USDC (for Puts) into the Thetanuts Finance v3 "Add Liquidity Module". Liquidity Providers will be entitled to option premiums from the Basic Vaults, lending interest from the Lending Market, trading fees from AMM, potential $NUTS token incentives, and potential additional token incentives.

**For Calls**\
\- 50% of $XYZ will be deposited into the Basic Vault to generate $XYZ-C, with $XYZ-C being Boosted in the Lending Market.\
\- 50% of $XYZ will be deposited into the $XYZ/$XYZ-C liquidity pool on Uniswap v3.

**For Puts**\
\- 50% of $USDC will be deposited into the Basic Vault to generate $XYZ-P, with $XYZ-P being Boosted in the Lending Market.\
\- 50% of $USDC will be deposited into the $XYZ-P/$USDC liquidity pool on Uniswap v3.

</details>

### 🤑 Traders

<details>

<summary>Why can't I Close Upon Expiry?</summary>

To facilitate a Close Upon Expiry, users pay a gas fee to put the position into a queue to be closed at the end of an epoch.

Positions are restricted from Close Upon Expiry if the position size is smaller than the gas fee required, which may cause the user’s P\&L to be negative after taking into account gas fees.

Alternatively, a user can also Close Now.

</details>

<details>

<summary>Can I partially close my long position?</summary>

Users are not able to partially close their long positions at the moment.

However, this is a high-priority feature that we will look to ship in the near future.

</details>

<details>

<summary>Why is Close Now unavailable?</summary>

Close Now is a feature that is only available for networks with the v3 Upgrade enabled.

At the moment, we have only enabled the v3 Upgrade for Arbitrum.

</details>

### 🌊 Liquidity Providers

<details>

<summary>What are the risks for using the Add Liquidity Module?</summary>

The Add Liquidity Module on Thetanuts Finance v3 is not a principal-protected product, and is subjected to a number of risks.

* **Risk of Impermanent Loss**: Given that a part of user collateral is deposited into the AMM, there is risk of impermanent loss.
* **Short Volatility Risk**: A part of user collateral is deposited into the Basic Vaults, which are short volatility positions.
* **Penalty Risk**: If there is insufficient liquidity in the lending market to redeem the collateral asset in the Add Liquidity Module, Thetanuts Finance will enable redemption of the collateral asset from the AMM instead. However, doing so will incur a penalty that scales up to 30% of the collateral value depending on the size of the redemption.

</details>

<details>

<summary>Why is there a periodic rebalancing in the AMM?</summary>

On the AMM, liquidity pool pairs are denominated as $XYZ/$XYZ-C for calls and $XYZ-P/$USDC for puts.

To open a long position on Thetanuts Finance v3, a user deposits $XYZ (for calls) or $USDC (for puts) into the lending market – and borrows $XYZ-C (for calls) or $XYZ-P (for puts. The borrowed Basic Vault LP Tokens are then sold on the AMM for $XYZ (for calls) and $USDC (for puts) to achieve the long position.

Opening a long position on Thetanuts Finance v3 thus increases the concentration of $XYZ-C (for calls) and $XYZ-P (for puts) in the liquidity pools, while reducing the concentration of $XYZ (for calls) and $USDC (for puts) – thus increasing the slippage for other users looking to open long positions and increasing the risk of impermanent loss for users looking to add liquidity.

Thetanuts Finance will periodically rebalance the liquidity pools by depositing additional $XYZ (for calls) and $USDC (for puts), which has the effect of improving execution Traders and Liquidity Providers.

To facilitate this, Thetanuts Finance charges a $1 fee for each rebalance event – whereby this fee is shared proportionally across users depending on their share of the Add Liquidity pool.

</details>

### 🥜 $NUTS

<details>

<summary>What is $NUTS?</summary>

$NUTS is the governance token of the Thetanuts Finance ecosystem, and will be used to achieve the long-term objectives of the protocol — including decentralization, governance, and incentives.

</details>

<details>

<summary>What is maximum supply of $NUTS?</summary>

$NUTS (ERC-20) will have a maximum supply of 10,000,000,000 $NUTS, and will have an initial circulating supply of \~7.5%.

</details>

<details>

<summary>What is the $NUTS contract address?</summary>

$NUTS is available for trading on both centralized and decentralized exchanges.\
\
The contract address for $NUTS is 0x23f3D4625AEF6f0b84d50dB1d53516e6015c0c9B.

</details>

<details>

<summary>How do I claim my $NUTS Season 1 Airdrop?</summary>

The Season 1 Airdrop can be claimed [here](https://app.thetanuts.finance/Airdrop).\
\
The Season 1 Airdrop is distributed as $veNUTS, which is subject to a 90-day vesting period. Users may initiate an Advanced Vesting with an exit penalty of 30 - 100%, of which this penalty will be sent to the Thetanuts Finance Treasury, in which the Thetanuts Finance DAO will have control over once governance is live.

</details>

### ❓ Others

<details>

<summary>Does Thetanuts Finance have any investors?</summary>

Yes. Thetanuts Finance raised a $18mm Seed round in April 2022, followed by a $17mm Series A round in April 2023.\
\
Our investors include Polychain Capital, QCP Capital, Deribit, Paradigm, GSR, Wintermute, Jump, GoldenTree Asset Management, Tribe Capital, and others.

</details>

<details>

<summary>Where are the verified contracts and were they audited?</summary>

The verified contracts may be viewed [here](/contracts-and-security/deployed-contracts), and all prior audits can be found [here](/contracts-and-security/security).

Thetanuts Finance has been audited by multiple leading security firms - including Peckshield, Zokyo, Akira Tech, and X41 D-Sec. No critical or major security flaws were found.

</details>

<details>

<summary>What is Lite Mode?</summary>

The Thetantus Finance v3 Lite Mode features a simple interface that allows users to easily express their bullish or bearish views using on-chain options. On Lite Mode, a user can go Long Call (if bullish) or Long Put (if bearish) — and can Close Now when they are satisified with the P\&L of their position.

To try Lite Mode, click on the "Lite" toggle on the Thetanuts Finance v3 Trading Interface.

</details>

<details>

<summary>Why were Stronghold, Long Vaults, and Degen Vaults brought in house?</summary>

As part of our transition to Thetanuts Finance v3 - we seek to streamline the user experience by bringing in-house certain products that have little synergy with our v3 upgrade, as we look to revamp them for the future.

Stronghold, Long Vaults, and Degen Vaults will enter into Withdrawal-only mode in November 2023. For more information, please visit [Legacy Products](/legacy-v3/other-products).

</details>


# Glossary of Terms

This section seeks to provide insights on options as an asset class, and in the context of Thetanuts Finance. The below terms will be important to understand Thetanuts Finance products.

<table><thead><tr><th width="154.74063057703748">Term</th><th width="570.4285714285713">Description</th></tr></thead><tbody><tr><td>Long Call</td><td>Buying a call option gives the right to buy the underlying asset at the strike price upon option expiry.</td></tr><tr><td>Long Put</td><td>Buying a put option gives the right to sell the underlying asset at the strike price upon option expiry.</td></tr><tr><td>Short Call</td><td>Selling a call option creates an obligation to sell the underlying asset at the strike price upon option expiry to the buyer of the call option. In exchange for selling the call option, writers (or sellers) receive an option premium.</td></tr><tr><td>Short Put</td><td>Selling a put option creates an obligation to buy the underlying asset at the strike price upon option expiry to the buyer of the put option. In exchange for selling the put option, writers (or sellers) receive an option premium.</td></tr><tr><td>Underlying Asset</td><td>The asset in which a derivative instrument (i.e. an option) is based off of.</td></tr><tr><td>Strike Price</td><td>The price at which the underlying asset can be bought or sold once an option is exercised.</td></tr><tr><td>Tenor</td><td>The length of time until option expiry. In the context of the Basic Vaults, users would see "Weekly" or "Bi-weekly" - indicating the tenor of option, along with a countdown to option expiry.</td></tr><tr><td>European Styled</td><td>Options that may only be exercised at the expiration date of the option, i.e. at a single pre-defined point in time.</td></tr><tr><td>Settlement</td><td>The process of fulfilling the terms of an options contract when the option is exercised — options may either be cash-settled or physically-settled.<br><br>In the context of the Basic Vaults, the option contract buyer (i.e. accredited market maker) will have the right to exercise the option contract if it is in-the-money ("ITM").</td></tr><tr><td>Cash Settlement</td><td>Settlement type that involves the delivery of the net cash amount on the settlement date if the option contract is exercised.</td></tr><tr><td>Physical Settlement</td><td>Settlement type that involves the physical delivery of the underlying asset (i.e. $ETH) on the settlement date if the option contract is exercised.</td></tr><tr><td>Epoch</td><td><p>An epoch is a period of time in which a Basic Vault is run. For a Basic Vault with Weekly tenor, each epoch will run for a period of one week.<br><br>An epoch is split into a few distinct phases:</p><ul><li><strong>Deposit Phase</strong>: The Basic Vaults collect deposits from users, or roll-over deposits that users have had from the previous epoch.</li><li><strong>Auction in progress</strong>: At the start of each epoch, Thetanuts Finance conducts a blind auction where accredited market makers bid to buy option contracts sold by the Basic Vaults.</li><li><strong>Settlement</strong>: The winning market maker of each Basic Vault then proceeds to deposit the option premiums upfront into the Basic Vault.</li><li><strong>In Progress</strong>: After settlement, the epoch begins.</li></ul></td></tr><tr><td>Basic Vault LP Tokens</td><td>$XYZ-C (Call Basic Vault LP Token) and $XYZ-P (Put Basic Vault LP Token) represent fungible and transferrable tokenized Basic Vault positions – and form the backbone of all trading strategies on Thetanuts Finance v3.</td></tr></tbody></table>


# Terms of Service

Updated November 18, 2023

Please read these Terms of Service (the “**Terms**”) carefully because they govern your use of the website located at <https://thetanuts.finance> (the “**Site**”). The Site is a decentralised on-chain options protocol (the “**Protocol**”) made available by Thetanuts Asset Corp (the “**Company**”, “**we**”, “**us**” or “**our**”). To make these Terms easier to read, the Site and our services are collectively called the “**Interface**”.

{% hint style="danger" %}
**NOTICE ON PROHIBITED USE – RESTRICTED PERSONS**: THE COMPANY PLATFORM AND ANY RELATED SERVICES ARE NOT OFFERED TO AND MAY NOT BE USED BY:

PERSONS OR ENTITIES WHO RESIDE IN, ARE CITIZENS OF, ARE LOCATED IN, ARE INCORPORATED IN, OR HAVE A REGISTERED OFFICE IN ANY RESTRICTED TERRITORY, AS DEFINED IN SECTION 3 (WHO MAY USE THE INTERFACE) (EACH SUCH PERSON OR ENTITY FROM A RESTRICTED TERRITORY, A “**RESTRICTED PERSON**”).

WE DO NOT MAKE EXCEPTIONS. THEREFORE, IF YOU ARE A RESTRICTED PERSON, THEN DO NOT ATTEMPT TO USE THE COMPANY PLATFORM OR ANY RELATED SERVICES. USE OF A VIRTUAL PRIVATE NETWORK (“**VPN**”) OR ANY OTHER SIMILAR MEANS INTENDED TO CIRCUMVENT THE RESTRICTIONS SET FORTH HEREIN IS PROHIBITED.
{% endhint %}

**WHEN YOU AGREE TO THESE TERMS, YOU ARE AGREEING (WITH LIMITED EXCEPTION) TO RESOLVE ANY DISPUTE BETWEEN YOU AND The Company THROUGH BINDING, INDIVIDUAL ARBITRATION RATHER THAN IN COURT. PLEASE REVIEW CAREFULLY SECTION 15 (DISPUTE RESOLUTION) BELOW FOR DETAILS REGARDING ARBITRATION. HOWEVER, IF YOU ARE A RESIDENT OF A JURISDICTION WHERE APPLICABLE LAW PROHIBITS ARBITRATION OF DISPUTES, THE AGREEMENT TO ARBITRATE IN SECTION 15 WILL NOT APPLY TO YOU BUT THE PROVISIONS OF SECTION 14 (GOVERNING LAW AND FORUM CHOICE) WILL APPLY INSTEAD.**

### 1.  Agreement to Terms

By using our Interface, you agree to be bound by these Terms. If you don’t agree to be bound by these Terms, then you must not use the Interface or access the Site. Notwithstanding the foregoing, the laws of some jurisdictions may limit or not permit certain provisions of this agreement, such as indemnification, the exclusion of certain warranties or the limitation of liability. In such a case, such provisions will apply only to the maximum extent permitted by the laws of such jurisdictions. Also, you may have additional legal rights in your jurisdiction, and nothing in these terms will prejudice such rights that you may have as a consumer of the Interface under such applicable law.

### 2. Changes to these Terms or the Interface

We may update these Terms from time to time in our sole discretion. If we do, we’ll let you know by posting the updated Terms on the Site and/or may also send other communications. It’s important that you review these Terms whenever we update them or you use the Interface. If you continue to use the Interface after we have posted updated Terms, it means that you accept and agree to the changes. If you don’t agree to be bound by the changes, you may not use the Interface anymore. Because our Interface is evolving over time, we may change or discontinue all or any part of the Interface, at any time and without notice, at our sole discretion.

### 3.  Who May Use the Interface?

You may use the Interface only if you are at least 18 years of age (or such other minimum age at which you can provide consent to data processing under the laws of your territory), and not otherwise barred from using the Interface under applicable law. In order to protect the integrity of the Interface, we reserve the right, at any time, in our sole discretion, to block access to the Interface from certain IP addresses and unique device identifiers. For the purposes of these Terms, “Restricted Territory” means of Algeria, Bangladesh, Bolivia, Belarus, Burundi, Burma (Myanmar), Canada, Cote D'Ivoire (Ivory Coast), Crimea and Sevastopol, Cuba, Democratic Republic of Congo, Ecuador, Iran, Iraq, Liberia, Libya, Mali, Morocco, Nepal, North Korea, Somalia, Sudan, Syria, United States, Venezuela, Yemen, Zimbabwe or any other country to which Canada, Panama, the United States, the United Kingdom or the European Union embargoes goods or imposes similar sanctions.

### 4. About the Interface

1. **Interface**. The Site provides an interface by which Users may access the Protocol, which is a fully decentralized, non-custodial, community-governed lending protocol deployed on multiple blockchain networks and systems, and provides information about the Company’s ecosystem, governance, community, and various interfaces and integrations to the Interface. All information provided in connection with your access and use of the Interface is for informational purposes only. You should not take, or refrain from taking, any action based on any information contained on the Site or any other information that we make available at any time, including blog posts, data, articles, links to third-party content, Discord content, Discourse content, news feeds, tutorials, tweets, and videos. Before you make any financial, legal, technical, or other decisions involving the Interface, you should seek independent professional advice from a licensed and qualified individual in the area for which such advice would be appropriate. Because the Interface provides information about the Protocol, these Terms also provide some information about the use of the Protocol. This information is not intended to be comprehensive or address all aspects of the Protocol.
2. **Our Relationship**.  You acknowledge and agree that the Company is an online platform provider and not a broker-dealer or a Lender. The Company does not direct or control the day-to-day activities of the users accessing the Site. Neither we nor any affiliated entity is a party to any transaction on the blockchain networks underlying the Protocol; we do not have possession, custody or control over any cryptoassets appearing on the Interface; and we do not have possession, custody, or control over any User’s funds or cryptoassets. Further, we do not store, send, or receive any funds or cryptoassets. You understand that when you interact with any Protocol smart contracts, you retain control over your cryptoassets at all times. The private key associated with the wallet address from which you transfer cryptoassets or the private key associated is the only private key that can control the cryptoassets you transfer into the smart contracts. You alone are responsible for securing your private keys. We do not have access to your private keys. Due to the non-custodial and decentralized nature of the technology, we are not intermediaries, agents, advisors, or custodians, and we do not have a fiduciary relationship or obligation to you regarding any other decisions or activities that you affect when using our Services. You acknowledge that we, for the avoidance of doubt, do not have any information regarding any Users, Users’ identities, or services beyond what is available or obtainable publicly via the blockchain. We are not responsible for any activities you engage in when using the Interface, and you should understand the risks associated with cryptoassets, cryptoassets, blockchain technology generally, and the Interface.
3. **Interface**. The Site provides an interface by which Users may access the Protocol. The Protocol is deployed on multiple blockchain-based networks, and we are not responsible for the operation of such networks. The software underlying blockchain networks on which the Protocol is deployed, including, for example, the Ethereum blockchain, is open source, which means that anyone can use, utilize, and build on top of it. By using the Interface, you acknowledge and agree (i) that we are not responsible for the operation of the blockchain-based software and networks underlying the Protocol, (ii) that there exists no guarantee of the functionality, security, or availability of that software and networks, and (iii) that the underlying blockchain-based networks are subject to sudden changes in operating rules, such as those commonly referred to as “forks”.
4. **Blockchain Fees**. There may be associated blockchain fees. All transactions using blockchains require the payment of gas fees, which are essentially transaction fees paid on every transaction that occurs on the selected blockchain network. Please note that gas fees are non-refundable.
5. **NUTS Rewards**. You may receive token rewards in the form of NUTS in the future. In order to receive NUTS rewards, you must first connect a third-party validated digital asset wallet.
6. **Liquidation**. Borrowers may lose some or all of their collateral in certain circumstances, including if their collateral value does not properly cover their loan/debt value (“Liquidation”). Upon Liquidation, Borrowers may also incur a penalty fee. Users may seek out loans that are eligible for Liquidation and pay back part of the debt owed in exchange for receiving discounted collateral.
7. **Communication from the Interface**. You may elect to receive emails or other communications from the Company in connection with the Interface. If you elect receive communications in connection with our Services, you agree and acknowledge that the Company is under no obligation to send any emails or communicate with you regarding such Loan or the expiration of such Loan, and that the Company will not be responsible or liable to you for any loss and takes no responsibility for and will not be liable to you for in the case of failure of such email or communication.
8. **Disputes Between Borrowers and Lenders**. The Company is not a party to any relationship between any Borrowers and/or Lenders or in the delivery or completion of borrowing/lending transactions. In the event that you have a dispute with any user of the Site, including any Borrowers or Lenders, you agree to address such dispute directly with such user. If permitted in your jurisdiction, you release the Company (and the Company’s officers, directors, agents, investors, subsidiaries, and employees) (collectively “Releasees”) from, and covenant not to sue Releasees for any and all claims, demands, or damages (actual or consequential) of any kind and nature, known and unknown, suspected and unsuspected, disclosed and undisclosed, arising out of or in any way connected with such dispute. As a Lender, to the degree that any collateral placed by a Borrower is a fraud, scam, or a non-authenticated asset, you agree and acknowledge that the Company and the Interface will not be responsible by any Loan made by you in reliance of collateral placed on the Interface.
9. **Regulatory and Compliance Suspensions or Terminations**. We may suspend or terminate your access to the Interface at any time in connection with any transaction as required by applicable law, any governmental authority, or if we in our sole and reasonable discretion determine you are violating these Terms or the terms of any third-party service provider. Such suspension or termination shall not be constituted a breach of these Terms by the Company. In accordance with its anti-money laundering, anti-terrorism, anti-fraud, and other compliance policies and practices, we may impose reasonable limitations and controls on the ability of you or any beneficiary to utilize the Interface. Such limitations may include, where good cause exists, rejecting transaction requests, freezing funds, or otherwise restricting you from using the Interface.

### 5. Content

1. **The Company’s Intellectual Property**. We may make available through the Interface content that is subject to intellectual property rights. We retain all rights to that content.
2. **Feedback**. We appreciate feedback, comments, ideas, proposals and suggestions for improvements to the Interface (“Feedback”). If you choose to submit Feedback, you agree that we are free to use it (and permit others to use it) without any restriction or compensation to you.

### 6. General Prohibitions and the Company’s Enforcement Rights

You agree not to do any of the following:

1. Post, upload, publish, submit or transmit any content that: (i) infringes, misappropriates or violates a third party’s patent, copyright, trademark, trade secret, moral rights or other intellectual property rights, or rights of publicity or privacy; (ii) violates, or encourages any conduct that would violate, any applicable law or regulation or would give rise to civil liability; (iii) is fraudulent, false, misleading or deceptive; (iv) is defamatory, obscene, pornographic, vulgar or offensive; (v) promotes discrimination, bigotry, racism, hatred, harassment or harm against any individual or group; (vi) is violent or threatening or promotes violence or actions that are threatening to any person or entity; or (vii) promotes illegal or harmful activities or substances;
2. Use, display, mirror or frame the Interface or any individual element within the Interface, the Company’s name, any Company trademark, logo or other proprietary information, or the layout and design of any page or form contained on a page, without the Company’s express written consent;
3. Access, tamper with, or use non-public areas of the Interface, the Company’s computer systems, or the technical delivery systems of the Company’s providers;
4. Attempt to probe, scan or test the vulnerability of any Company system or network or breach any security or authentication measures;
5. Avoid, bypass, remove, deactivate, impair, descramble or otherwise circumvent any technological measure implemented by the Company or any of the Company’s providers or any other third party (including another user) to protect the Interface;
6. Attempt to access or search the Interface or download content from the Interface using any engine, software, tool, agent, device or mechanism (including spiders, robots, crawlers, data mining tools or the like) other than the software and/or search agents provided by the Company or other generally available third-party web browsers;
7. Send any unsolicited or unauthorized advertising, promotional materials, email, junk mail, spam, chain letters or other form of solicitation;
8. Use any meta tags or other hidden text or metadata utilizing Company trademark, logo URL or product name without the Company’s express written consent;
9. Use the Interface, or any portion thereof, for any commercial purpose or for the benefit of any third party or in any manner not permitted by these Terms;
10. Forge any TCP/IP packet header or any part of the header information in any email or newsgroup posting, or in any way use the Interface to send altered, deceptive or false source-identifying information;
11. Attempt to decipher, decompile, disassemble or reverse engineer any of the software used to provide the Interface;
12. Interfere with, or attempt to interfere with, the access of any user, host or network, including, without limitation, sending a virus, overloading, flooding, spamming, or mail-bombing the Interface;
13. Collect or store any personally identifiable information from the Interface from other users of the Interface without their express permission;
14. Impersonate or misrepresent your affiliation with any person or entity;
15. Use NUTS for capital raising purposes (“**Capital Raising**”). Capital Raising is defined as receiving funds from the sale of NUTS, where the primary purpose of the sale of NUTS is to raise capital for you or another party and not to obtain Services within the Protocol or the Interface. You acknowledge that any NUTS you receive is solely for use within the Protocol or the Interface and not for speculative purposes;
16. Violate any applicable law, rule, or regulation concerning the integrity of trading markets, including (but not limited to) the manipulative tactics commonly known as spoofing and wash trading;
17. Violate any applicable law or regulation; or
18. Encourage or enable any other individual to do any of the foregoing.

The Company is not obligated to monitor access to or use of the Interface or to review or edit any content. However, we have the right to do so for the purpose of operating the Interface, to ensure compliance with these Terms and to comply with applicable law or other legal requirements. We reserve the right, but are not obligated, to remove or disable access to any content, including User Content, at any time and without notice, including, but not limited to, if we, at our sole discretion, consider it objectionable or in violation of these Terms. If we have a reasonable belief that you have created multiple accounts or have associated the same digital asset wallet to multiple accounts, we reserve the right to disable your access to the Interface. If you believe we have erroneously disabled your access, you may contact us at <hello@thetanuts.finance>. We have the right to investigate violations of these Terms or conduct that affects the Interface. We may also consult and cooperate with law enforcement authorities to prosecute users who violate the law.

### 7. No Fiduciary Duties

The Interface is not intended to, and does not, create or impose any fiduciary duties on the Company. To the fullest extent permitted by law, any user of the Interface acknowledges and agrees that the Company owes no fiduciary duties or liabilities it or any other party, and that to the extent any such duties or liabilities may exist at law or in equity, those duties and liabilities are hereby irrevocably disclaimed, waived, and eliminated. Any user of the Interface further agrees that the only duties and obligations that the Company may owe are those set out expressly herein.

### 8. Copyright Policy

The Company respects copyright law and expects its users to do the same. It is the Company’s policy to terminate access in appropriate circumstances for account holders who repeatedly infringe or are believed to be repeatedly infringing the rights of copyright holders.

### 9. Links to Third Party Websites or Resources

The Interface may allow you to access third-party websites or other resources. We provide access only as a convenience and are not responsible for the content, products or services on or available from those resources or links displayed on such websites. You acknowledge sole responsibility for and assume all risk arising from your use of any third-party resources.

### 10. Termination

We may suspend or terminate your access to and use of the Interface, including suspending access to your account, at our sole discretion, at any time and without notice to you. You may cancel your access at any time by sending us an email at <hello@thetanuts.finance>. Upon any termination, discontinuation or cancellation of the Interface or your access to the Interface, the following Sections, and such other provisions that by their nature are intended to survive, will survive: 4, 5, 6, 7, 10, 11, 12, 13, 14, 15 and 16.

### 11. Warranty Disclaimers

The Services are provided “As Is,” without warranty of any kind. Without limiting the foregoing, we explicitly disclaim any implied warranties of merchantability, fitness for a particular purpose, quiet enjoyment and non-infringement, and any warranties arising out of course of dealing or usage of trade. We make no warranty that the Interface will meet your requirements or be available on an uninterrupted, secure, or error-free basis. We make no warranty regarding the quality, accuracy, timeliness, truthfulness, completeness or reliability of any information or content on the Interface.

The Company will not be responsible or liable to you for any loss and takes no responsibility for, and will not be liable to you for, any use of the services, including but not limited to any losses, damages or claims arising from: (i) user error such as forgotten passwords, incorrectly constructed transactions, or mistyped wallet addresses; (ii) server failure or data loss; (iii) cryptocurrency wallets or corrupt files; (iv) unauthorized access to services; or (v) any third party activities, including without limitation the use of viruses, phishing, bruteforcing or other means of attack against any blockchain network underlying the services.

By accessing and using the Interface, you represent that you understand the inherent risks associated with using cryptographic and blockchain-based systems, and that you have a working knowledge of the usage and intricacies of cryptoassets and other digital assets such as bitcoin (BTC), ether (ETH), USD Coin (USDC) and other digital tokens such as those following the Ethereum Token Standard (ERC-20). You further understand that the markets for these cryptoassets are highly volatile due to factors including (but not limited to) adoption, speculation, technology, security, and regulation. You acknowledge that the cost and speed of transacting with cryptographic and blockchain-based systems are variable and may increase at any time. You further acknowledge the risk that your cryptoassets may lose some or all of their value while they are supplied to or from the Interface. You further acknowledge that we are not responsible for any of these variables or risks and cannot be held liable for any resulting losses that you experience while accessing Interface. Accordingly, you understand and agree to assume full responsibility for all of the risks of accessing, using and interacting with the Interface.

### 12. Indemnity

You will indemnify and hold the Company and its officers, directors, employees and agents harmless from and against any claims, disputes, demands, liabilities, damages, losses, costs and expenses, including, without limitation, reasonable legal and accounting fees arising out of or in any way connected with (a) your access to or use of the Interface, (b) any content that you post or otherwise make available through the Interface, or (c) your violation of these Terms.

### 13. Limitation of Liability

1. To the maximum extent permitted by law, neither the Company nor its service providers involved in creating, producing, or delivering the services will be liable for any incidental, special, exemplary or consequential damages, or damages for lost profits, lost revenues, lost savings, lost business opportunity, loss of data or goodwill, service interruption, computer damage or system failure or the cost of substitute services of any kind arising out of or in connection with these terms or from the use of or inability to use the services, whether based on warranty, contract, tort (including negligence), product liability or any other legal theory, and whether or not the company or its service providers have been informed of the possibility of such damage, even if a limited remedy set forth herein is found to have failed of its essential purpose.
2. To the maximum extent permitted by the law of the applicable jurisdiction, in no event will the Company’s total liability arising out of or in connection with these terms or from the use of or inability to use the services exceed, as applicable, the amounts you have paid or are payable by you to the Company for use of the Services or, if you have not had any payment obligations to the Company, one hundred dollars ($100).
3. The exclusions and limitations of damages set forth above are fundamental elements of the basis of the bargain between the Company and you.

### 14. Governing Law and Forum Choice

These Terms and any action related thereto will be governed by the laws of Panama, without regard to its conflict of laws provisions. Except as otherwise expressly set forth in Section 15 (Dispute Resolution), the exclusive jurisdiction for all Disputes (defined below) that you and the Company are not required to arbitrate will be the courts located in the Republic of Singapore, and you and the Company each waive any objection to jurisdiction and venue in such courts.

### 15. Dispute Resolution

These Terms shall be governed by, and construed in accordance with, the laws of the Republic of Singapore. Any dispute arising out of or in connection with these Terms, including any question regarding its existence, validity or termination, shall be referred to and finally be resolved by arbitration in Singapore in accordance with the rules of the SIAC for the time being in force, which rules are deemed to be incorporated by reference in this paragraph. The seat of the arbitration shall be Singapore. The tribunal shall consist of a sole arbitrator to be appointed by the Chairman of the SIAC. The language of the arbitration shall be English. This arbitration agreement shall be governed by the laws of the Republic of Singapore. Each of the Parties irrevocably submits to the non-exclusive jurisdiction of the courts of Singapore to support and assist the arbitration process, including if necessary the grant of interlocutory relief pending the outcome of that process.

### 16. General Terms

1. **Reservation of Rights**. The Company and its licensors exclusively own all right, title and interest in and to the Interface, including all associated intellectual property rights. You acknowledge that the Interface is protected by copyright, trademark, and other laws of the United States and foreign countries. You agree not to remove, alter or obscure any copyright, trademark, service mark or other proprietary rights notices incorporated in or accompanying the Interface.
2. **Entire Agreement**. These Terms constitute the entire and exclusive understanding and agreement between the Company and you regarding the Interface, and these Terms supersede and replace all prior oral or written understandings or agreements between the Company and you regarding the Interface. If any provision of these Terms is held invalid or unenforceable by an arbitrator or a court of competent jurisdiction, that provision will be enforced to the maximum extent permissible and the other provisions of these Terms will remain in full force and effect. You may not assign or transfer these Terms, by operation of law or otherwise, without the Company’s prior written consent. Any attempt by you to assign or transfer these Terms, without such consent, will be null. The Company may freely assign or transfer these Terms without restriction. Subject to the foregoing, these Terms will bind and inure to the benefit of the parties, their successors and permitted assigns.
3. **Notices**. Any notices or other communications provided by the Company under these Terms will be given: (i) via email; or (ii) by posting to the Interface. For notices made by email, the date of receipt will be deemed the date on which such notice is transmitted.
4. **Waiver of Rights**. The Company’s failure to enforce any right or provision of these Terms will not be considered a waiver of such right or provision. The waiver of any such right or provision will be effective only if in writing and signed by a duly authorized representative of the Company. Except as expressly set forth in these Terms, the exercise by either party of any of its remedies under these Terms will be without prejudice to its other remedies under these Terms or otherwise.

### 17. Contact Information

If you have any questions about these Terms or the Interface, please contact the Company at <hello@thetanuts.finance>.

<br>


# Brand Assets

<figure><img src="/files/A36FTcNGXKocgYp7WnbM" alt=""><figcaption></figcaption></figure>

### SVG

{% file src="/files/k3UBRl7V6Q1kt1Bfj53k" %}

{% file src="/files/rCBIhKcKTqRqOk7yJR45" %}

{% file src="/files/0XLO4Q1jE5CE8oIteQeU" %}

{% file src="/files/lMCRR2RnLYmJSvloV4Gy" %}

{% file src="/files/QO7ysnqWaEBF6MzQ48RV" %}

### PNG

{% file src="/files/7BOBhFS0ioXmNwaGD1M8" %}

{% file src="/files/mxA2VGLiQRMn0eIruy87" %}

{% file src="/files/rrcHL9Ifizbyd7im7tVH" %}

{% file src="/files/bsSA1mhDpxEmS0B0MCXO" %}

{% file src="/files/uijY0hf8olHVIxAi4shX" %}


# Overview

### Thetanuts Finance v3 Upgrade

Our first-to-market v3 architecture uses our existing Basic Vault LP Tokens to extend new use cases in on-chain options trading — enabled by a Lending Market and Uniswap v3 Pools, which are all housed within the v3 interface.

$XYZ-C (Call Basic Vault LP Token) and $XYZ-P (Put Basic Vault LP Token) represent fungible and transferrable tokenized Basic Vault positions – and form the backbone of all trading strategies on Thetanuts Finance v3.

<figure><img src="/files/9aJqnkT08gxBPsye8V9o" alt=""><figcaption></figcaption></figure>

### Benefits of the Thetanuts Finance v3 Upgrade

* **Basic Vaults Remain the Core of Thetanuts Finance v3**: Basic Vaults will continue to be at the core of Thetanuts Finance, with the Basic Vault LP tokens being used to bootstrap the rest of the v3 ecosystem. This allows Thetanuts Finance to leverage their prior track record of leading Option Vault protocol focused on altcoins.
* **Housing v3 under a Unified Interface**: v3 will feature an upgraded trading-like interface, abstracting the need for traders to interact individually with Basic Vaults, AMM, and the Lending / Borrowing Market.
* **Ability to Buy Options**: Option Vault strategies mainly center around the sell-side, with no ability for users to go long — v3 will change this through the use of the AMM and Lending Market.
* **Flexibility to Exit Basic Vault LP Positions**: Prior to v3, Vault LPs would need to wait until the end of the next epoch — implying that they could not exit their positions until a week later. With the AMM, a user could simply swap out of the Basic Vault LP token — instantly closing their position without having to wait for the end of the epoch.
* **Battle-tested Infrastructure:** Aave v2 was referenced during the creation of the Thetanuts Finance v3 Lending Market, with the protocol never being exploited prior. Combined with the Thetanuts Finance and Uniswap v3 tech stack that has never been exploited, the Thetanuts Finance team is confident of the security and safety of the v3 upgrade.
* **Additional Sources of Yield and Incentives:** Through Thetanuts Finance v3, users have access to additional sources of yield beyond just option premiums from the Basic Vaults. These additional sources include: lending interest from the Lending Market, trading fees from AMM, potential $NUTS token incentives, and potential additional token incentives.
* **Protocol Solvency**: Basic Vaults in Thetanuts Finance are 100% collateralized.
* **Free Market Pricing:** Unlike other option protocols where IV is used as an input to price options, v3 features free market pricing — whereby if users believe that AMM pricing is incorrect, they can arbitrage this opportunity for profit.


# Basic Vaults

### Basic Vaults Overview

Basic Vaults sell out-of-money (“OTM”) European cash-settled options to accredited market makers, and generate yields for users in the form of option premiums. Vaults have pre-defined parameters (inc. strike price, delta, and tenor) – creating specific risk-adjusted yields on each vault, while also abstracting the complexity in users needing to design these parameters on their own.

<figure><img src="/files/8nQS0QeL2Pv7vDU82Vrr" alt=""><figcaption></figcaption></figure>

Basic Vault LP Tokens represent short volatility instruments, either as short call or short put positions:\
\- **Selling covered calls (Short Call)**: Designed to generate income for users.\
\- **Selling cash-secured puts (Short Put)**: Designed to accumulate the underlying asset for users.

Basic Vault collateral denominations are based on the instrument:\
\- **For calls**: Denominated in the underlying asset (i.e. $XYZ)\
\- **For puts**: Denominated in $USDC

### Basic Vault Auctions & Settlement

Every Friday, Thetanuts Finance conducts blind auctions for the Basic Vaults off-chain - where whitelisted accredited market makers bid to buy option contracts sold by the Basic Vaults. The winning market maker of each Basic Vault then proceeds to deposit the option premiums upfront into the Basic Vault.

Upon expiry of the contract,  Thetanuts Finance references major centralised exchanges as a data source to determine settlement price - including Deribit Index (i.e. for majors), Binance Spot (i.e. for altcoins), and others (i.e. for longer tail assets). This settlement price determines whether the option contract has been struck. The Basic Vault would then settle with the winning market maker of that epoch, depending on whether the option contract is struck.

### Basic Vaults in Thetanuts Finance v3

$XYZ-C (Call Basic Vault LP Token) and $XYZ-P (Put Basic Vault LP Token) represent fungible and transferrable tokenized Basic Vault positions – and form the backbone of all trading strategies on Thetanuts Finance v3.

Users that have deposited into the Basic Vaults will mint a Basic Vault LP Token, and can redeem the Basic Vault LP Token by initiating a withdrawal of collateral from their Basic Vault position. This Mint / Redeem mechanism also gives rise to a “**Mint Price**”, defined as the price of a Basic Vault LP Token when minted or redeemed from a Basic Vault.

<figure><img src="/files/ihzLCkXegl9JXkNYZD76" alt=""><figcaption></figcaption></figure>

These Basic Vault LP Tokens may be used to:

* **Provide Liquidity on Lending Market**: Basic Vault LP tokens can be deposited into the Lending Market to generate additional lending interest.
* **Provide Liquidity on AMM**: Basic Vault LP tokens can be deposited into the AMM to generate additional AMM trading fees.
* **Go Long**: Users interested in a long volatility position may borrow the Basic Vault LP Tokens from the Lending Market and sell them on the AMM, representing a long call or long put position.
* **Close Basic Vault Position Instantly**: A Basic Vault LP Token holder may sell the position on the AMM, representing an instant close of the short call or short put position without having to wait for the end of the Basic Vault epoch.


# Lending Market

### Lending Market Overview

The v3 Lending Market will be housed within the v3 interface, with contracts being inspired by Aave v2. The Lending Market is a core module within Thetanuts Finance v3, enabling users to go long or short on on-chain options.

### Parameters for the Lending Market

* **Supported Assets**: The Lending Market will support $XYZ-C, $XYZ-P, $XYZ, and $USDC assets.
* **Flash Loans**: Similar to Aave v2, the Lending Market will enable flash loans - allowing users to borrow up to 95% LTV, implying a theoretical 20x leverage limit on long positions. In reality - the leverage on a long position may be lower due to slippage, price impact, and fees on the AMM.
* **Interest Rates**: The Lending Market will use a fixed interest rate model between epochs, which can be changed in the future based on governance.

### Actors in the Lending Market

* **Lenders**: Lenders can deposit Basic Vault LP Tokens (i.e. $XYZ-C, $XYZ-P) into the Lending Market via "Boost", and receive Lending Interest from Borrowers.
* **Borrowers**
  * **For Long Positions**: Under the hood, users that open long positions on Thetanuts Finance v3 are essentially borrowing Basic Vault LP Tokens from the Lending Market and selling them on the AMM.
  * **For Capital Efficiency**: Users that have supplied collateral into the Lending Market can borrow against their Lending Market positions.

### Using the Lending Market

After a user opens a short call or short put position, the user will be prompted to Boost – which deposits the Basic Vault LP Tokens into the Lending Market. By providing liquidity on the Lending Market, Lenders generate yields in the form of Lending Interest.

<figure><img src="/files/SBYy4ud29tfCpX9cYjXW" alt=""><figcaption></figcaption></figure>

Lenders on the Lending Market may then borrow against their positions, borrowing $XYZ or $USDC depending on the pools that they have supplied. This enables users to achieve greater capital efficiency on their short position trading collateral, and allowing them to use borrowed capital from the Lending Market to interact with other components of the Thetanuts Finance v3 ecosystem.

<figure><img src="/files/yJKA1ZNEnbhGebXpZD0w" alt=""><figcaption></figcaption></figure>


# Automated Market Maker

### AMM Overview

Thetanuts Finance v3 will utilize battle-tested Uniswap v3 pools to enable users to go long or short on on-chain options, and will form a key part Thetanuts Finance v3.

### Parameters for the AMM

* **Uniswap v3 Liquidity Pools**: Liquidity pool pairs will be denominated as $XYZ/$XYZ-C for Calls, and $XYZ-P/$USDC for Puts.
* **AMM Price**: These pools also gives rise to an “**AMM Price**”, defined as the price of a Basic Vault LP Token as set by the relative concentration of assets within the liquidity pool given by the x\*y=k.

### Actors in the AMM

* **Liquidity Providers**: Liquidity Providers provide TVL, enabling Traders to go long or short on on-chain options. Liquidity Providers receive a share of AMM Trading Fees.
* **Traders**: With these Uniswap v3 liquidity pools, Traders can take on long volatility or short volatility positions.

### Adding Liquidity into the AMM via the Add Liquidity Module

To Add Liquidity into the AMM, a Liquidity Provider would deposit $XYZ (for Calls) or $USDC (for Puts) into the Thetanuts Finance v3 Add Liquidity Module. Liquidity Providers will be entitled to option premiums from the Basic Vaults, lending interest from the Lending Market, trading fees from AMM, potential $NUTS token incentives, and potential additional token incentives.

<figure><img src="/files/iCG8tBE1uzBPNDHzKZR1" alt=""><figcaption></figcaption></figure>

**If a user has a bearish bias, a user can deposit $XYZ into the Add Liquidity Module**\
\- Part of the $XYZ will be deposited into the Basic Vault to generate $XYZ-C (Short Call Basic Vault LP Token), with $XYZ-C being Boosted in the Lending Market.\
\- Part of the $XYZ will be deposited into the $XYZ/$XYZ-C liquidity pool on Uniswap v3.

**If a user has a bullish bias, a user can deposit $USDC into the Add Liquidity Module**\
\- Part of the $USDC will be deposited into the Basic Vault to generate $XYZ-P (Short Put Basic Vault LP Token), with $XYZ-P being Boosted in the Lending Market.\
\- Part of the $USDC will be deposited into the $XYZ-P/$USDC liquidity pool on Uniswap v3.


# Mechanics

### Overview of Mechanics

While strategies on Thetanuts Finance have traditionally centred around the sell-side, Thetanuts Finance v3 will enable buy-side strategies as well - allowing users to go long or short on on-chain options. This section will cover the mechanics of a Long, Short, Close Now, and Close Upon Expiry trade lifecycle.

### Short Volatility

A short volatility position on Thetanuts Finance represents holding of a Basic Vault LP Token.\
\- **Short Call**: $XYZ swapped for $XYZ-C on the AMM, with $XYZ-C representing a short call.\
\- **Short Put**: $USDC swapped for $XYZ-P on the AMM, with $XYZ-P representing a short put.

### Long Volatility

A long volatility position on Thetanuts Finance sees a number of steps within the v3 architecture in order to achieve long exposure.\
\
**Long Call**\
$XYZ collateral deposited into the v3 Lending Market. A flash loan for $XYZ-C occurs at a 95% LTV on the Lending Market, with up to 20x of collateral value borrowed. Borrowed $XYZ-C is sold for $XYZ in the AMM, achieving a long call position.

**Long Put**\
$USDC collateral is supplied into the v3 Lending Market. A flash loan of $XYZ-P occurs at a 95% LTV on the Lending Market, with up to 20x of collateral value borrowed. Borrowed $XYZ-P is sold for $XYZ in the AMM, achieving a long put position.

{% hint style="info" %}
**What happens when there is insufficient liquidity to Long Call or Long Put?**

* If there is insufficient $XYZ-C / ($XYZ-P) in the Lending Market, a long call / (long put) trade may fail.
  {% endhint %}

### Close Now

Users on Thetanuts Finance v3 can opt to Close Now on their open positions, which has the effect of closing their positions instantly.&#x20;

**Short Call**\
Swap $XYZ-C to $XYZ on the AMM. If there is insufficient liquidity in the AMM to facilitate a Close Now, the user can instead close the position Upon Expiry instead.

**Short Put**\
Swap $XYZ-P to $USDC on the AMM. If there is insufficient liquidity in the AMM to facilitate a Close Now, the user can instead close the position Upon Expiry instead.

**Long Call**\
Thetanuts Finance v3 will route the order to the venue with the best pricing, by querying both the Basic Vaults (for Mint Price) and the AMM (for AMM Price).

* If AMM Price is more favourable, $XYZ is sold in the AMM for $XYZ-C, with the $XYZ-C loan then being returned to the Lending market – thereafter, the initial $XYZ position is then withdrawn from the Lending market.
* If Mint Price is more favourable, $XYZ will be deposited into the Basic Vault to mint $XYZ-C, with the loan being returned and $XYZ collateral in the Lending market redeemed.
* $XYZ in excess of loan repaid represents P\&L.

**Long Put**\
Thetanuts Finance v3 will route the order to the venue with the best pricing, by querying both the Basic Vaults (for Mint Price) and the AMM (for AMM Price).

* If AMM Price is more favourable, $USDC is sold in the AMM for $XYZ-P, with the $XYZ-P loan then being returned to the Lending market – thereafter, the initial $USDC position is then withdrawn from the Lending market.
* If Mint Price is more favourable, $USDC will be deposited into the Basic Vault to mint $XYZ-P, with the loan being returned and $USDC collateral in the Lending market redeemed.
* $USDC in excess of loan repaid represents P\&L.

### Close Upon Expiry

Alternatively, users on Thetanuts Finance v3 can opt to Close Upon Expiry their open positions - which closes the position at the end of the epoch.

**Short Call**\
The user closes the Short Call (i.e. $XYZ-C) at the end of the Basic Vault epoch, and claims the collateral after the epoch ends.\
\
**Short Put**\
The user closes the Short Put (i.e. $XYZ-P) at the end of the Basic Vault epoch, and claims the collateral after the epoch ends.

**Long Call**\
At the end of the epoch, $XYZ position is deposited into the Basic Vault to generate $XYZ-C – which is used to repay any outstanding debt in the Lending Market. $XYZ in excess of loan repaid represents P\&L.

**Long Put**\
At the end of the epoch, $USDC position is deposited into the Basic Vault to generate $XYZ-P – which is used to repay any outstanding debt in the Lending Market. $USDC in excess of loan repaid represents P\&L.

### Considerations for Long Positions on Thetanuts Finance v3

* **Theoretical Leverage**: While the theoretical leverage based on a 95% LTV is 20x, leverage on Thetanuts Finance v3 is typically in the range of 15–20x — as a result of slippage, price impact, and fees on the AMM.
* **Managing Borrowing Costs**: Leveraging in this manner carries inherent risks. The triggers for a soft close of a user's position on Thetanuts Finance v3 are the accumulating costs from borrowing (i.e. option premiums) rather than market volatility — where a soft close occurs when the user runs out of collateral. This is possible given that value of the borrowed asset (i.e. XYZ-C or XYZ-P) can increase due to option premiums and lending interest payments, and the fact that each lending position utilizes the underlying asset as collateral.
* **Automatic Rollover**: Positions not closed at the end of the epoch will automatically roll over to the new epoch, with any P\&L captured in the user’s net position in the lending market.
* **Protocol Solvency**: Basic Vaults in Thetanuts Finance are 100% collateralized.

### Arbitrage Opportunities on Thetanuts Finance v3

The same Basic Vault LP Token will have both a Mint Price and an AMM Price, which may result in an arbitrage opportunity if they do not trade at the same level. This scenario occurs when the Mint Price > AMM Price for a Basic Vault LP Token, where a user could arbitrage this by:

* **For Calls:** Deposit $XYZ in the Basic Vault to mint $XYZ-C; with exposure on Thetanuts Finance v3 achieved by going short call on XYZ. Sell $XYZ-C directly on the AMM, receiving $XYZ.
* **For Puts:** Deposit $USDC in the Basic Vault to mint $XYZ-P; with exposure on Thetanuts Finance v3 achieved by going short put on XYZ. Sell $XYZ-P directly on the AMM, receiving $USDC.


# Other Products

As part of our transition to Thetanuts Finance v3 - we seek to streamline the user experience by bringing in-house certain products that have little synergy with our v3 upgrade, as we look to revamp them for the future.

### **Stronghold**

* Index product, where $USDC is deposited into various Basic Vaults for diversification.
* Stronghold entered into Withdrawal-only mode in Nov 2023.
* You can access Stronghold for withdrawal [here](https://legacy.thetanuts.finance/stronghold).

### Long Vaults

* Vaults taking the buy-side of a Basic Vault, allowing users to long calls or puts.
* Long Vaults entered into Withdrawal-only mode in Nov 2023.
* You can access Long Vaults for withdrawal [here](https://legacy.thetanuts.finance/long-vault).

### **Degen Vaults**

* Vaults running spreads and condor strategies.
* Degen Vaults entered into Withdrawal-only mode in Nov 2023.
* You can access Degen Vaults for withdrawal [here](https://legacy.thetanuts.finance/degen-vault).


# Overview

TypeScript SDK for Thetanuts Finance V4 — options trading on EVM chains.

> **Using an LLM (Claude, Cursor, ChatGPT)?** This SDK ships an MCP server that exposes \~100 tools — read state, build transactions, run pricing math. One-line install: `npx -y @thetanuts-finance/mcp`, or paste [the LLM context prompt](/sdk/resources/llm-context) into your LLM. Want to actually **trade** from chat? Pair it with Base MCP and approve each transaction in your wallet ([Trade from Chat](/sdk/ai-agents/base-mcp-plugin)), or run a fully autonomous agent with its own wallet ([AgentKit](/sdk/ai-agents/agentkit)). Start at [AI Agents — Pick Your Route](/sdk/ai-agents/agents-overview).

## Features

* **Complete Options Trading**: Fill orders, manage positions, handle RFQs
* **Settlement Types**: Cash-settled and physically settled options
* **Multi-Strategy Support**: Spreads, butterflies, condors, iron condors (cash-settled)
* **Real-time Data**: WebSocket subscriptions for live updates
* **Type-Safe**: Full TypeScript support with comprehensive type definitions
* **Modular Design**: Use only what you need
* **ESM and CJS builds**: Maximum compatibility

## OptionBook vs RFQ

The SDK supports two trading systems. Choose based on your use case:

|                 | **OptionBook**                                                               | **RFQ (Factory)**                                                                                 |
| --------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **What**        | Fill existing market-maker orders                                            | Create custom options via sealed-bid auction                                                      |
| **When to use** | Quick trades on listed options (vanilla and multi-leg)                       | Custom options via sealed-bid auction: any strike, any expiry, cash-settled or physically settled |
| **Structures**  | Vanilla, spread, butterfly, condor, iron condor (cash-settled)               | Vanilla, spread, butterfly, condor, iron condor (cash-settled or physically settled for vanilla)  |
| **Key methods** | `fillOrder()`, `previewFillOrder()`                                          | `buildRFQRequest()`, `requestForQuotation()`                                                      |
| **Pricing**     | Order prices from `fetchOrders()`                                            | MM pricing from `getAllPricing()`                                                                 |
| **Data source** | Book indexer (`/api/v1/book/`)                                               | Factory indexer (`/api/v1/factory/`)                                                              |
| **User data**   | `getUserPositionsFromIndexer()`                                              | `getUserRfqs()`, `getUserOptionsFromRfq()`                                                        |
| **Stats**       | `getBookProtocolStats()`, `getBookDailyStats()`                              | `getFactoryProtocolStats()`, `getFactoryDailyStats()`                                             |
| **Collateral**  | Paid upfront by taker                                                        | `collateralAmount = 0` (held by factory)                                                          |
| **Settlement**  | Cash-settled (payout in USDC/WETH/cbBTC based on price difference at expiry) | Cash-settled by default; physically settled optional via `buildPhysicalOptionRFQ()`               |

## Which one should I use?

Both OptionBook and RFQ create cash-settled options using the same on-chain contracts. The difference is how you get there:

```
Is there an existing order that matches your trade?
  YES --> Use OptionBook (instant fill, no waiting)
  NO  --> Use RFQ (custom strike/expiry, MMs compete on price)

Need physical settlement (actual token delivery at expiry)?
  YES --> Use RFQ with buildPhysicalOptionRFQ()
  NO  --> Either works. OptionBook is faster if an order exists.
```

**OptionBook** is like a limit order book. Makers have already posted orders with set strikes, expiries, and prices. You pick one and fill it instantly.

**RFQ** is like sending out a request for bids. You specify what you want (any strike, any expiry), and market makers compete via sealed-bid auction. It takes \~60 seconds for offers to arrive, but you get competitive pricing on exactly the parameters you need.

***

## See also

* [Installation](/sdk/getting-started/installation)
* [Quick Start](/sdk/getting-started/quick-start)
* [Supported Chains](/sdk/getting-started/supported-chains)
* [MCP Server](/sdk/ai-agents/mcp-server) — Claude / Cursor / VS Code one-click install
* [LLM Context](/sdk/resources/llm-context) — copy-paste prompt to teach any LLM the full SDK


# Installation

Add the Thetanuts SDK to your project using npm or yarn.

> **Just want to use this with an LLM?** Skip the SDK install. The [MCP server](/sdk/ai-agents/mcp-server) gives Claude / Cursor / VS Code one-click access to \~100 SDK tools without writing code: `npx -y @thetanuts-finance/mcp`.

## Install the package

Using npm:

```bash
npm install @thetanuts-finance/thetanuts-client
```

Using yarn:

```bash
yarn add @thetanuts-finance/thetanuts-client
```

## Compatibility

| Requirement | Minimum Version |
| ----------- | --------------- |
| Node.js     | >= 18           |
| ethers.js   | v6              |
| TypeScript  | >= 5.0          |

## Build Formats

The SDK ships as both ESM and CJS with TypeScript declarations (`.d.ts`), built with [tsup](https://tsup.egoist.dev/). This means it works out of the box in:

* Node.js projects (CommonJS or ESM)
* TypeScript projects
* Bundlers (Vite, webpack, esbuild, etc.)

No additional build configuration is required.

***

## See also

* [Quick Start](/sdk/getting-started/quick-start)
* [Configuration](/sdk/getting-started/configuration)
* [Supported Chains](/sdk/getting-started/supported-chains)


# Quick Start

Get up and running with read-only access to the Thetanuts SDK in minutes — no signer required.

> **Skipping straight to LLM workflows?** Use the [MCP server](/sdk/ai-agents/mcp-server) (`npx -y @thetanuts-finance/mcp`) or paste the [LLM context prompt](/sdk/resources/llm-context) into Claude / Cursor / ChatGPT. The rest of this page covers direct TypeScript usage.

## Read-only example

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

// Initialize with provider (read-only — no signer needed for any of this)
const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const client = new ThetanutsClient({
  chainId: 8453, // Base mainnet
  provider,
});

// 1. Fetch available orders from the book
const orders = await client.api.fetchOrders();
console.log(`Found ${orders.length} orders`);

// 2. Get market data
const marketData = await client.api.getMarketData();
console.log(`BTC: $${marketData.prices.BTC}`);
console.log(`ETH: $${marketData.prices.ETH}`);

// 3. Preview what a fill would actually cost (no transaction, no signer)
//    For PUTs and spreads, contract count is NOT the same as premium —
//    previewFillOrder runs the same collateral math the contract uses.
if (orders.length > 0) {
  const preview = client.optionBook.previewFillOrder(orders[0], 10_000000n); // 10 USDC
  console.log(`Contracts: ${preview.numContracts}`);
  console.log(`Collateral: ${preview.collateralToken}`);
  console.log(`Price per contract: ${preview.pricePerContract}`);
}

// 4. Get MM pricing for custom RFQ options
const pricing = await client.mmPricing.getAllPricing('ETH');
const active = client.mmPricing.filterExpired(Object.values(pricing));
console.log(`${active.length} active ETH options available for RFQ`);
```

## Next steps

To actually execute a fill you need a signer. Jump to [OptionBook: browse and fill an order](/sdk/optionbook/overview), or to create a custom option jump to [RFQ: create a custom option](/sdk/rfq-factory/overview).

***

## See also

* [Configuration](/sdk/getting-started/configuration)
* [OptionBook overview](/sdk/optionbook/overview)
* [RFQ overview](/sdk/rfq-factory/overview)


# Configuration

All options you can pass when creating a `ThetanutsClient` instance, plus how to plug in a custom logger.

## ThetanutsClientConfig

```typescript
interface ThetanutsClientConfig {
  chainId: 8453;                    // Required: Chain ID
  provider: Provider;               // Required: ethers.js provider
  signer?: Signer;                  // Optional: For transactions
  referrer?: string;                // Optional: Referrer address for fees
  apiBaseUrl?: string;              // Optional: Override API URL
  indexerApiUrl?: string;           // Optional: Override indexer URL
  pricingApiUrl?: string;           // Optional: Override pricing URL
  wsUrl?: string;                   // Optional: Override WebSocket URL
  stateApiUrl?: string;             // Optional: Override RFQ state indexer URL
  env?: 'dev' | 'prod';             // Optional: Environment (default: prod)
  logger?: ThetanutsLogger;         // Optional: Custom logger
  keyStorageProvider?: KeyStorageProvider; // Optional: RFQ key storage override
  rfqKeyPrefix?: string;            // Optional: RFQ key namespace
}
```

## Custom Logger

Pass a custom logger to capture SDK debug output in your own monitoring system (Sentry, Datadog, etc.):

```typescript
import { ThetanutsClient, consoleLogger } from '@thetanuts-finance/thetanuts-client';

// Option 1: Use the built-in console logger
const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  logger: consoleLogger,
});

// Option 2: Provide your own logger implementation
const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  logger: {
    debug: (msg, meta) => myLogger.debug(msg, meta),
    info: (msg, meta) => myLogger.info(msg, meta),
    warn: (msg, meta) => myLogger.warn(msg, meta),
    error: (msg, meta) => myLogger.error(msg, meta),
  },
});
```

***

## See also

* [Installation](/sdk/getting-started/installation)
* [Quick Start](/sdk/getting-started/quick-start)
* [Supported Chains](/sdk/getting-started/supported-chains)


# Supported Chains

The chains where the Thetanuts SDK is currently available, and how to access per-chain contract addresses from the SDK.

## Supported Chains

| Chain            | Chain ID | Status                 | Notes                                                                                                                                                                                         |
| ---------------- | -------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Base Mainnet     | 8453     | Supported              | Full options surface (OptionBook, RFQ, Loan, StrategyVault, Ranger). Collar pricing/preview helpers are available; on-chain collar writes are gated until collar-v12 addresses are populated. |
| Ethereum Mainnet | 1        | Supported (vault-only) | WheelVault module only — OptionBook/RFQ are not deployed here                                                                                                                                 |

## Chain Configuration

Access all contract addresses and token configs from the client — no hardcoding required:

```typescript
const config = client.chainConfig;

// Tokens
config.tokens.USDC.address;  // 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
config.tokens.USDC.decimals; // 6
config.tokens.WETH.address;  // 0x4200000000000000000000000000000000000006
config.tokens.cbBTC.address; // 0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf

// Cash-settled implementations (OptionBook)
config.implementations.PUT;                  // Vanilla PUT
config.implementations.INVERSE_CALL;         // Vanilla CALL (inverse-priced)
config.implementations.LINEAR_CALL;          // Vanilla CALL (linear)
config.implementations.PUT_SPREAD;           // Put spread (2 strikes)
config.implementations.CALL_SPREAD;          // Call spread (2 strikes)
config.implementations.INVERSE_CALL_SPREAD;  // Inverse call spread (2 strikes)
config.implementations.PUT_FLY;              // Put butterfly (3 strikes)
config.implementations.CALL_FLY;             // Call butterfly (3 strikes)
config.implementations.PUT_CONDOR;           // Put condor (4 strikes)
config.implementations.CALL_CONDOR;          // Call condor (4 strikes)
config.implementations.IRON_CONDOR;          // Iron condor (4 strikes)
config.implementations.RANGER;               // Ranger / zone-bound (4 strikes, r12)
config.implementations.CALL_LOAN;            // Physically-settled call loan handler

// HistoricalPriceConsumerV3_TWAP (Chainlink TWAP consumer) — r12
config.twapConsumer; // string | null (null on chains without it)

// Physically settled implementations (RFQ/Factory)
config.implementations.PHYSICAL_PUT;          // Vanilla physical PUT
config.implementations.PHYSICAL_CALL;         // Vanilla physical CALL

// Price feeds
config.priceFeeds.ETH;  // Chainlink ETH/USD feed
config.priceFeeds.BTC;  // Chainlink BTC/USD feed
```

***

## See also

* [Overview](/sdk)
* [Installation](/sdk/getting-started/installation)
* [Configuration](/sdk/getting-started/configuration)


# Overview

Fill existing market-maker orders for options on-chain without needing a counterparty.

## What is OptionBook?

OptionBook is the order-book side of the Thetanuts protocol. Market makers post signed orders for options (vanilla PUTs/CALLs, spreads, butterflies, condors, and iron condors on ETH and BTC). As a taker, you browse those orders, pick one you like, and fill it in a single transaction.

The number of strikes in `order.strikes[]` tells you the product type:

* **1 strike** = Vanilla (PUT or CALL)
* **2 strikes** = Spread
* **3 strikes** = Butterfly
* **4 strikes** = Condor or Iron Condor

Collateral is paid upfront by the taker. All OptionBook options are **cash-settled**: at expiry, the payout is calculated based on the difference between the strike price and the settlement price (Chainlink oracle), and paid out in the collateral token:

* **PUT options**: Collateral and payout in USDC
* **CALL options (Inverse Call)**: Collateral and payout in the underlying token (WETH for ETH, cbBTC for BTC)

No physical delivery of assets occurs. For physically settled options (actual delivery of underlying at expiry), use [RFQ/Factory](/sdk/rfq-factory/overview) instead.

## OptionBook vs RFQ

|                 | **OptionBook**                                                               | **RFQ (Factory)**                                                                                 |
| --------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **What**        | Fill existing market-maker orders                                            | Create custom options via sealed-bid auction                                                      |
| **When to use** | Quick trades on listed options (vanilla and multi-leg)                       | Custom options via sealed-bid auction: any strike, any expiry, cash-settled or physically settled |
| **Structures**  | Vanilla, spread, butterfly, condor, iron condor (cash-settled)               | Vanilla, spread, butterfly, condor, iron condor (cash-settled or physically settled for vanilla)  |
| **Key methods** | `fillOrder()`, `previewFillOrder()`                                          | `buildRFQRequest()`, `requestForQuotation()`                                                      |
| **Pricing**     | Order prices from `fetchOrders()`                                            | MM pricing from `getAllPricing()`                                                                 |
| **Data source** | Book indexer (`/api/v1/book/`)                                               | Factory indexer (`/api/v1/factory/`)                                                              |
| **User data**   | `getUserPositionsFromIndexer()`                                              | `getUserRfqs()`, `getUserOptionsFromRfq()`                                                        |
| **Stats**       | `getBookProtocolStats()`, `getBookDailyStats()`                              | `getFactoryProtocolStats()`, `getFactoryDailyStats()`                                             |
| **Collateral**  | Paid upfront by taker                                                        | `collateralAmount = 0` (held by factory)                                                          |
| **Settlement**  | Cash-settled (payout in USDC/WETH/cbBTC based on price difference at expiry) | Cash-settled by default; physically settled optional via `buildPhysicalOptionRFQ()`               |

Both OptionBook and RFQ create cash-settled options using the same on-chain implementation contracts (PUT, INVERSE\_CALL, spreads, etc.). The difference is how you get there:

* **OptionBook**: Fill a pre-priced order instantly. The strike, expiry, and price are already set by the maker.
* **RFQ**: Request your own custom strike and expiry. Market makers compete via sealed-bid auction (\~60 seconds for offers).

If OptionBook has an order matching your trade, use it. It's faster. Use RFQ when you need parameters that aren't currently listed.

## Core Flow

Every OptionBook trade follows the same four steps:

```
fetchOrders()          — browse available maker orders
    ↓
previewFillOrder()     — dry-run: see contracts, collateral, price per contract
    ↓
ensureAllowance()      — approve the collateral token
    ↓
fillOrder()            — execute the trade on-chain
```

## Quick Example

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

const client = new ThetanutsClient({
  chainId: 8453, // Base mainnet
  provider,
  signer,
});

// 1. Fetch available orders
const orders = await client.api.fetchOrders();
const order = orders.find((o) => o.order.expiry > BigInt(Math.floor(Date.now() / 1000)));
if (!order) throw new Error('No active orders');

// 2. Preview the fill (no transaction, no signer needed)
const preview = client.optionBook.previewFillOrder(order, 10_000000n); // 10 USDC
console.log(`Contracts: ${preview.numContracts}`);
console.log(`Collateral token: ${preview.collateralToken}`);
console.log(`Price per contract: ${preview.pricePerContract}`);

// 3. Approve collateral spending
await client.erc20.ensureAllowance(
  client.chainConfig.tokens.USDC.address,
  client.chainConfig.contracts.optionBook,
  10_000000n,
);

// 4. Fill the order
const receipt = await client.optionBook.fillOrder(order, 10_000000n);
console.log(`Trade executed: ${receipt.hash}`);
```

## OptionBook Implementations

All OptionBook options are cash-settled. The implementation address on each order determines the product type:

Use `client.chainConfig.implementations` for the current Base r12 implementation addresses. Orders from `fetchOrders()` may reference current or earlier implementation versions; the SDK reverse lookup handles both transparently.

## Data Source

OptionBook orders come from the Book indexer at `/api/v1/book/`. The SDK abstracts this — you call `client.api.fetchOrders()` and receive typed `OrderWithSignature` objects with pre-computed Greeks and metadata.

## Module Access

All OptionBook operations go through two modules:

| Module              | Purpose                                                   |
| ------------------- | --------------------------------------------------------- |
| `client.api`        | Fetch orders, positions, stats (read-only, no signer)     |
| `client.optionBook` | Fill/cancel orders, claim fees (write ops require signer) |

***

## See Also

* [Browse and Filter Orders](/sdk/optionbook/browse-filter-orders)
* [Preview Fills](/sdk/optionbook/preview-fills)
* [Fill Orders](/sdk/optionbook/fill-orders)


# Browse & Filter Orders

Fetch live maker orders from the OptionBook and narrow them down to exactly what you want to trade.

## fetchOrders()

Returns all currently available orders from the Book indexer. No signer required.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const client = new ThetanutsClient({ chainId: 8453, provider });

const orders = await client.api.fetchOrders();
console.log(`Found ${orders.length} orders`);
```

Each element is an `OrderWithSignature` object. The most useful fields are on `order.order` (the on-chain struct) and `order.rawApiData` (enriched metadata from the indexer including Greeks).

### OrderWithSignature shape

```typescript
// Key fields you'll use most often
const o = orders[0];

o.order.expiry          // bigint — Unix timestamp
o.order.availableAmount // bigint — maker's collateral budget (NOT contract count)
o.order.price           // bigint — price per contract (8 decimals)
o.order.isBuy           // boolean — from the maker's perspective

o.rawApiData?.greeks?.delta  // number — delta
o.rawApiData?.greeks?.iv     // number — implied volatility (0–1)
o.rawApiData?.greeks?.theta  // number — theta per day
o.rawApiData?.isCall         // boolean
o.rawApiData?.strikes        // bigint[] — strike(s) in 8 decimals
o.rawApiData?.underlying     // 'ETH' | 'BTC'
```

> `availableAmount` is the maker's collateral budget, not the number of contracts. A 10,000 USDC budget at a $95,000 strike gives you roughly 0.105 contracts. Always use `previewFillOrder()` to see the actual contract count.

## filterOrders()

Fetch orders pre-filtered by the API. Saves you from loading the full order list when you already know what you want.

```typescript
// Only CALL orders that haven't expired
const callOrders = await client.api.filterOrders({
  isCall: true,
  minExpiry: Math.floor(Date.now() / 1000),
});

// Only PUT orders
const putOrders = await client.api.filterOrders({ isCall: false });
```

**Filter criteria:**

| Field       | Type      | Description                             |
| ----------- | --------- | --------------------------------------- |
| `isCall`    | `boolean` | `true` for CALLs, `false` for PUTs      |
| `minExpiry` | `number`  | Minimum expiry timestamp (Unix seconds) |

## Finding a specific order

After fetching, use standard array methods to locate the order you want:

```typescript
const orders = await client.api.fetchOrders();
const now = BigInt(Math.floor(Date.now() / 1000));

// Find the first non-expired order
const order = orders.find((o) => o.order.expiry > now);
if (!order) throw new Error('No active orders found');

// Find a specific ETH PUT by strike
const ethPut2000 = orders.find(
  (o) =>
    o.rawApiData?.underlying === 'ETH' &&
    !o.rawApiData?.isCall &&
    o.rawApiData?.strikes?.[0] === 200000000000n, // $2000 in 8 decimals
);

// Sort by price ascending
const byPrice = [...orders].sort((a, b) =>
  Number(a.order.price - b.order.price)
);
```

## OptionBook API methods

Full list of read methods available via `client.api` for the OptionBook side:

| Method                                 | Description                                |
| -------------------------------------- | ------------------------------------------ |
| `fetchOrders()`                        | All available orders                       |
| `filterOrders(criteria)`               | Orders matching filter criteria            |
| `getUserPositionsFromIndexer(address)` | User positions on the book side            |
| `getUserHistoryFromIndexer(address)`   | Trade history on the book side             |
| `getBookOption(optionAddress)`         | Single book option with PnL                |
| `getReferrerStatsFromIndexer(address)` | Referrer stats (book side)                 |
| `getBookProtocolStats()`               | Protocol stats with 24h/7d/30d windows     |
| `getBookDailyStats()`                  | Daily time series data                     |
| `getStatsFromIndexer()`                | Legacy totals (uniqueUsers, openPositions) |

## Greeks from order data

```typescript
const orders = await client.api.fetchOrders();

for (const order of orders) {
  if (order.rawApiData?.greeks) {
    const { delta, iv, gamma, theta, vega } = order.rawApiData.greeks;
    console.log(`Delta: ${delta.toFixed(4)}`);
    console.log(`IV: ${(iv * 100).toFixed(1)}%`);
    console.log(`Theta: ${theta.toFixed(4)}/day`);
  }
}
```

Greeks may be `null` for illiquid options or very short-dated options near expiry.

***

## Indexer stat helpers

Beyond order browsing, the `client.api` module exposes aggregate read endpoints backed by the same indexer. All are read-only.

```typescript
// OptionBook market metadata (orderbook depth, last prices, expiry buckets)
const market = await client.api.getMarketData();

// OptionFactory (RFQ) state
const activeRfqs = await client.api.getFactoryRfqs('active');
const factoryStats = await client.api.getFactoryStats();

// Per-referrer accrual on the OptionFactory side (paired with
// docs/rfq/referrals.md). Useful for dashboards and audit trails.
const referrerStats = await client.api.getFactoryReferrerStats(referrerAddress);
```

For protocol-wide aggregates, use `client.api.getProtocolStats()` (book + factory combined) or its scoped siblings `getBookProtocolStats()` / `getFactoryProtocolStats()`. Daily breakdowns are exposed via `getDailyStats()`.

***

## See Also

* [Overview](/sdk/optionbook/overview)
* [Preview Fills](/sdk/optionbook/preview-fills)
* [Fill Orders](/sdk/optionbook/fill-orders)


# Multi-Leg Structures

Fill existing multi-leg orders from the OptionBook, including spreads, butterflies, condors, and iron condors.

## How It Works

OptionBook orders can contain 1 to 4 strikes. The number of strikes tells you the product type:

```
order.strikes.length === 1  →  VANILLA    (single option)
order.strikes.length === 2  →  SPREAD     (2-leg)
order.strikes.length === 3  →  BUTTERFLY  (3-leg)
order.strikes.length === 4  →  CONDOR or IRON CONDOR  (4-leg)
```

You don't need to handle multi-leg differently in code. The same `previewFillOrder()` and `fillOrder()` methods work for all structures. The SDK reads the implementation address and strike count from the order and calculates collateral accordingly.

> **All OptionBook options are cash-settled.** For physically settled options, use [RFQ](/sdk/rfq-factory/physical-options).

***

## Identifying Multi-Leg Orders

When you fetch orders from the book, you can identify the structure by checking `strikes.length` or the implementation address:

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const client = new ThetanutsClient({ chainId: 8453, provider });

const orders = await client.api.fetchOrders();

for (const order of orders) {
  const strikes = order.rawApiData?.strikes ?? [];
  const impl = order.rawApiData?.implementation ?? '';
  const isCall = order.rawApiData?.isCall;

  let structure = 'UNKNOWN';
  if (strikes.length === 1) structure = isCall ? 'VANILLA CALL' : 'VANILLA PUT';
  if (strikes.length === 2) structure = isCall ? 'CALL SPREAD' : 'PUT SPREAD';
  if (strikes.length === 3) structure = isCall ? 'CALL BUTTERFLY' : 'PUT BUTTERFLY';
  if (strikes.length === 4) structure = 'CONDOR / IRON CONDOR';

  console.log(`${structure} | ${strikes.length} strikes | impl: ${impl}`);
}
```

### Filtering by Structure

```typescript
// Get only spread orders
const spreads = orders.filter((o) => (o.rawApiData?.strikes ?? []).length === 2);

// Get only butterfly orders
const butterflies = orders.filter((o) => (o.rawApiData?.strikes ?? []).length === 3);

// Get only condor/iron condor orders
const condors = orders.filter((o) => (o.rawApiData?.strikes ?? []).length === 4);

// Get only vanilla orders
const vanillas = orders.filter((o) => (o.rawApiData?.strikes ?? []).length === 1);
```

***

## Structures Summary

| Structure    | Strikes | Implementation               | Collateral Formula                 |
| ------------ | ------- | ---------------------------- | ---------------------------------- |
| Vanilla PUT  | 1       | `PUT`                        | `(collateral x 1e8) / strike`      |
| Vanilla CALL | 1       | `INVERSE_CALL`               | `collateral / 1e12`                |
| Spread       | 2       | `PUT_SPREAD` / `CALL_SPREAD` | `(collateral x 1e8) / spreadWidth` |
| Butterfly    | 3       | `PUT_FLY` / `CALL_FLY`       | `(collateral x 1e8) / maxSpread`   |
| Condor       | 4       | `PUT_CONDOR` / `CALL_CONDOR` | `(collateral x 1e8) / maxSpread`   |
| Iron Condor  | 4       | `IRON_CONDOR`                | `(collateral x 1e8) / maxSpread`   |

***

## Preview and Fill a Multi-Leg Order

The workflow is identical to vanilla orders. `previewFillOrder()` automatically uses the correct collateral formula based on the strike count.

### Spread Example

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

// 1. Find a spread order (2 strikes)
const orders = await client.api.fetchOrders();
const spreadOrder = orders.find((o) => (o.rawApiData?.strikes ?? []).length === 2);

if (!spreadOrder) throw new Error('No spread orders available');

// 2. Preview the fill
const preview = client.optionBook.previewFillOrder(spreadOrder, 50_000000n); // 50 USDC
console.log(`Contracts: ${preview.numContracts}`);
console.log(`Max contracts: ${preview.maxContracts}`);
console.log(`Strikes: ${preview.strikes.join(', ')}`);
console.log(`Collateral token: ${preview.collateralToken}`);

// 3. Approve collateral
await client.erc20.ensureAllowance(
  preview.collateralToken,
  client.chainConfig.contracts.optionBook,
  preview.totalCollateral,
);

// 4. Fill
const receipt = await client.optionBook.fillOrder(spreadOrder, 50_000000n);
console.log(`Spread filled: ${receipt.hash}`);
```

### Butterfly Example

```typescript
// Find a butterfly order (3 strikes)
const butterflyOrder = orders.find((o) => (o.rawApiData?.strikes ?? []).length === 3);

if (butterflyOrder) {
  const preview = client.optionBook.previewFillOrder(butterflyOrder);
  console.log(`Butterfly: ${preview.strikes.length} strikes`);
  console.log(`Max contracts: ${preview.maxContracts}`);
  console.log(`Price per contract: ${preview.pricePerContract}`);

  // Approve and fill
  await client.erc20.ensureAllowance(
    preview.collateralToken,
    client.chainConfig.contracts.optionBook,
    preview.totalCollateral,
  );
  const receipt = await client.optionBook.fillOrder(butterflyOrder);
  console.log(`Butterfly filled: ${receipt.hash}`);
}
```

### Condor Example

```typescript
// Find a condor order (4 strikes, not iron condor)
const condorOrder = orders.find((o) => {
  const strikes = o.rawApiData?.strikes ?? [];
  const impl = o.rawApiData?.implementation?.toLowerCase() ?? '';
  // Condor has 4 strikes but is NOT an iron condor
  return strikes.length === 4 && !impl.includes('iron');
});

if (condorOrder) {
  const preview = client.optionBook.previewFillOrder(condorOrder);
  console.log(`Condor: ${preview.strikes.length} strikes`);
  console.log(`Strikes: ${preview.strikes.join(', ')}`);
  console.log(`Max contracts: ${preview.maxContracts}`);
  console.log(`Price per contract: ${preview.pricePerContract}`);

  // Approve and fill
  await client.erc20.ensureAllowance(
    preview.collateralToken,
    client.chainConfig.contracts.optionBook,
    preview.totalCollateral,
  );
  const receipt = await client.optionBook.fillOrder(condorOrder);
  console.log(`Condor filled: ${receipt.hash}`);
}
```

### Iron Condor Example

An iron condor combines a put spread and a call spread. It uses the `IRON_CONDOR` implementation with 4 strikes.

```typescript
// Find an iron condor order
// Iron condors use a specific implementation address
const ironCondorImpl = client.chainConfig.implementations.IRON_CONDOR.toLowerCase();

const ironCondorOrder = orders.find((o) => {
  const strikes = o.rawApiData?.strikes ?? [];
  const impl = (o.rawApiData?.implementation ?? '').toLowerCase();
  return strikes.length === 4 && impl === ironCondorImpl;
});

if (ironCondorOrder) {
  const preview = client.optionBook.previewFillOrder(ironCondorOrder);
  console.log(`Iron Condor: ${preview.strikes.length} strikes`);
  console.log(`Strikes: ${preview.strikes.join(', ')}`);
  console.log(`Max contracts: ${preview.maxContracts}`);

  // Approve and fill
  await client.erc20.ensureAllowance(
    preview.collateralToken,
    client.chainConfig.contracts.optionBook,
    preview.totalCollateral,
  );
  const receipt = await client.optionBook.fillOrder(ironCondorOrder);
  console.log(`Iron Condor filled: ${receipt.hash}`);
}
```

***

## Implementation Addresses

These are the cash-settled implementation contracts used by OptionBook orders:

Use `client.chainConfig.implementations` for the current Base r12 implementation addresses. The SDK also keeps reverse-lookup metadata for older implementation versions that may appear in indexed orders.

Orders from `fetchOrders()` may also reference earlier implementation versions. The SDK handles both transparently.

***

## OptionBook vs RFQ for Multi-Leg

Both systems support the same multi-leg structures using the same implementation contracts.

|                | OptionBook                           | RFQ                                               |
| -------------- | ------------------------------------ | ------------------------------------------------- |
| **How**        | Fill an existing order from the book | Create a new option with custom parameters        |
| **Speed**      | Instant fill                         | \~60 second auction                               |
| **Parameters** | Fixed by the maker                   | You choose strike(s) and expiry                   |
| **Settlement** | Cash-settled only                    | Cash-settled or physically settled (vanilla only) |
| **SDK method** | `fillOrder(order)`                   | `buildRFQRequest({ strikes: [...] })`             |

If there's a multi-leg order on the book that matches your trade, use OptionBook. If you need custom strikes or expiry, use [RFQ multi-leg](/sdk/rfq-factory/multi-leg-structures).

***

## See Also

* [Preview Fills](/sdk/optionbook/preview-fills) - Collateral formulas and preview details
* [Fill Orders](/sdk/optionbook/fill-orders) - Execute a fill
* [Browse & Filter Orders](/sdk/optionbook/browse-filter-orders) - Find orders on the book
* [RFQ Multi-Leg Structures](/sdk/rfq-factory/multi-leg-structures) - Create custom multi-leg options


# Preview Fills

Simulate a fill before committing — see the exact contract count, collateral required, and price per contract without sending any transaction.

## Why preview first?

For OptionBook orders, `availableAmount` on the order is the **maker's collateral budget**, not the number of contracts you receive. The actual contract count depends on the option type and its collateral formula. Without previewing, you cannot know what you are actually buying.

`previewFillOrder()` runs the exact formula the on-chain contract will use, so there are no surprises at fill time.

## Collateral Formulas by Option Type

| Option Type      | Strikes | Formula                            | Example                                      |
| ---------------- | ------- | ---------------------------------- | -------------------------------------------- |
| **Vanilla PUT**  | 1       | `(collateral × 1e8) / strike`      | 10,000 USDC at $95k strike = 0.105 contracts |
| **Inverse CALL** | 1       | `collateral / 1e12`                | 1 WETH = 1 contract                          |
| **SPREAD**       | 2       | `(collateral × 1e8) / spreadWidth` | 10,000 USDC / $10k spread = 1 contract       |
| **BUTTERFLY**    | 3       | `(collateral × 1e8) / maxSpread`   | Based on widest strike range                 |
| **CONDOR**       | 4       | `(collateral × 1e8) / maxSpread`   | Based on widest strike range                 |

**Why this matters for a PUT:** A $95,000 strike PUT where the maker budgets 10,000 USDC:

* `(10,000 × 1e8) / 95,000 = ~0.105 contracts`
* You are buying 0.105 contracts, **not** 10,000

## previewFillOrder()

Dry-run a fill. Safe to call without a signer — no chain state is modified.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const client = new ThetanutsClient({ chainId: 8453, provider });

const orders = await client.api.fetchOrders();
const order = orders[0];

// Preview a max fill (no amount specified)
const maxPreview = client.optionBook.previewFillOrder(order);
console.log(`Max contracts available: ${maxPreview.maxContracts}`);
console.log(`Collateral token: ${maxPreview.collateralToken}`);
console.log(`Price per contract: ${maxPreview.pricePerContract}`);

// Preview with a specific premium amount (10 USDC)
const preview = client.optionBook.previewFillOrder(order, 10_000000n);
console.log(`Contracts for 10 USDC: ${preview.numContracts}`);
console.log(`Total collateral needed: ${preview.totalCollateral}`);
```

### Parameters

| Parameter      | Type                 | Description                                                           |
| -------------- | -------------------- | --------------------------------------------------------------------- |
| `orderWithSig` | `OrderWithSignature` | Order from `client.api.fetchOrders()`                                 |
| `usdcAmount`   | `bigint?`            | Collateral to spend (6 decimals). Omit to preview a max fill          |
| `referrer`     | `string?`            | Referrer address. Falls back to client-level referrer or zero address |

### Return value

```typescript
{
  numContracts: bigint,      // Contracts you will receive for the given usdcAmount
  maxContracts: bigint,      // Maximum contracts available in this order
  collateralToken: string,   // Token address for collateral payment
  pricePerContract: bigint,  // Price per contract (8 decimals)
  totalCollateral: bigint,   // Total collateral needed (use this for ensureAllowance)
  referrer: string,          // Resolved referrer address
  maker: string,             // Maker address
  expiry: bigint,            // Order expiry timestamp
  isCall: boolean,           // true for CALL, false for PUT
  strikes: bigint[],         // Strike(s) in 8 decimals
}
```

## Full preview-to-fill pattern

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

const client = new ThetanutsClient({ chainId: 8453, provider, signer });

// 1. Fetch orders
const orders = await client.api.fetchOrders();
const order = orders.find((o) => o.order.expiry > BigInt(Math.floor(Date.now() / 1000)));
if (!order) throw new Error('No active orders');

// 2. Preview with 10 USDC
const preview = client.optionBook.previewFillOrder(order, 10_000000n);

console.log(`You will receive: ${preview.numContracts} contracts`);
console.log(`Price per contract: ${preview.pricePerContract}`);
console.log(`Collateral required: ${preview.totalCollateral}`);
console.log(`Collateral token: ${preview.collateralToken}`);

// 3. Use preview.totalCollateral for the exact approval amount
await client.erc20.ensureAllowance(
  preview.collateralToken,
  client.chainConfig.contracts.optionBook,
  preview.totalCollateral,
);

// 4. Fill
const receipt = await client.optionBook.fillOrder(order, 10_000000n);
console.log(`Filled: ${receipt.hash}`);
```

## Checking order expiry before preview

```typescript
const now = BigInt(Math.floor(Date.now() / 1000));

if (order.order.expiry <= now) {
  throw new Error('Order has already expired');
}

// Safe to preview
const preview = client.optionBook.previewFillOrder(order, 10_000000n);
```

***

## On-chain pre-flight (callStatic)

`previewFillOrder()` runs the math locally. If you also want to confirm the contract itself would not revert under current state (allowance, nonce reuse, order cancellation, transient reverts) without paying gas, use the `callStatic` helpers — they execute the call against the node and return the would-be result or the revert reason.

```typescript
// Pre-flight a fill: returns the option address that would be created/transferred.
const fillResult = await client.optionBook.callStaticFillOrder(order, 10_000000n);
if (!fillResult.ok) {
  console.error('Fill would revert:', fillResult.error);
} else {
  console.log('Would create option at:', fillResult.value);
}

// Pre-flight a cancel.
const cancelResult = await client.optionBook.callStaticCancelOrder(order);
if (!cancelResult.ok) {
  console.error('Cancel would revert:', cancelResult.error);
}
```

Both methods return a `CallStaticResult` (`{ ok: true, value }` or `{ ok: false, error }`). Useful inside form UIs to disable the submit button when the chain says the call would fail.

***

## See Also

* [Browse and Filter Orders](/sdk/optionbook/browse-filter-orders)
* [Fill Orders](/sdk/optionbook/fill-orders)
* [Overview](/sdk/optionbook/overview)


# Fill Orders

Execute a trade against a maker order, swap-and-fill in one transaction, or cancel your own order.

## Setup: client with signer

All write operations (fill, cancel) require a signer. Read operations (preview, fetch) do not.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  signer,
  referrer: '0x92b8ac05b63472d1D84b32bDFBBf3e1887331567', // optional
});
```

## Complete OptionBook fill workflow

```typescript
// 1. Fetch available orders
const orders = await client.api.fetchOrders();
const order = orders.find((o) => o.order.expiry > BigInt(Math.floor(Date.now() / 1000)));
if (!order) throw new Error('No active orders found');

// 2. Preview the fill (dry-run, no transaction)
const preview = client.optionBook.previewFillOrder(order, 10_000000n); // 10 USDC
console.log(`Contracts: ${preview.numContracts}, Collateral: ${preview.totalCollateral}`);

// 3. Approve collateral spending
await client.erc20.ensureAllowance(
  client.chainConfig.tokens.USDC.address,
  client.chainConfig.contracts.optionBook,
  10_000000n,
);

// 4. Fill the order
const receipt = await client.optionBook.fillOrder(order, 10_000000n);
console.log(`Trade executed: ${receipt.hash}`);
```

## fillOrder()

Execute a fill against an existing maker order.

```typescript
const receipt = await client.optionBook.fillOrder(
  order,                           // OrderWithSignature from fetchOrders()
  10_000000n,                      // 10 USDC (optional — omit to fill max)
  '0xYourReferrerAddress',         // optional referrer override
);
console.log(`Filled: ${receipt.hash}`);
```

**Parameters:**

| Parameter      | Type                 | Description                                                              |
| -------------- | -------------------- | ------------------------------------------------------------------------ |
| `orderWithSig` | `OrderWithSignature` | Order from `client.api.fetchOrders()`                                    |
| `usdcAmount`   | `bigint?`            | Collateral to spend (6 decimals). Omit to fill the full available amount |
| `referrer`     | `string?`            | Referrer address. Falls back to client-level referrer or zero address    |

**Returns:** `TransactionReceipt`

**Throws:**

* `ORDER_EXPIRED` — order has already expired
* `INVALID_ORDER` — order is missing `rawApiData` (re-fetch from the API)
* `SIGNER_REQUIRED` — no signer configured on the client
* `INSUFFICIENT_ALLOWANCE` — approve the collateral token first via `client.erc20.ensureAllowance()`
* `CONTRACT_REVERT` — on-chain revert (check `error.cause` for details)

## swapAndFillOrder()

Fill an order while atomically swapping from a different source token. Useful when you hold WETH but the order requires USDC collateral.

```typescript
const receipt = await client.optionBook.swapAndFillOrder(
  order,
  swapRouterAddress,       // e.g. 1inch, 0x, Odos router address
  swapSrcTokenAddress,     // token you are holding (e.g. WETH address)
  swapSrcAmount,           // amount of source token to swap (bigint)
  swapCalldata,            // pre-encoded swap calldata from your aggregator
  '0xYourReferrerAddress', // optional
);
```

**Parameters:**

| Parameter       | Type                 | Description                                |
| --------------- | -------------------- | ------------------------------------------ |
| `orderWithSig`  | `OrderWithSignature` | Order from `client.api.fetchOrders()`      |
| `swapRouter`    | `string`             | Swap router contract address               |
| `swapSrcToken`  | `string`             | Source token address to swap from          |
| `swapSrcAmount` | `bigint`             | Amount of source token to swap             |
| `swapData`      | `string`             | Encoded swap calldata from your aggregator |
| `referrer`      | `string?`            | Referrer address                           |

**Returns:** `TransactionReceipt`

You must approve the swap router to spend the source token before calling this. The encoded swap calldata must match `swapRouter`, `swapSrcToken`, and `swapSrcAmount` exactly.

## cancelOrder()

Cancel an existing order. Only the original maker can cancel their own orders.

```typescript
const receipt = await client.optionBook.cancelOrder(order);
console.log(`Cancelled: ${receipt.hash}`);
```

**Parameters:** `orderWithSig: OrderWithSignature`

**Returns:** `TransactionReceipt`

**Throws:** `INVALID_ORDER` if the order is missing `rawApiData`. Contract reverts if the caller is not the original maker.

## Error handling

```typescript
import {
  ThetanutsError,
  OrderExpiredError,
  InsufficientAllowanceError,
  ContractRevertError,
} from '@thetanuts-finance/thetanuts-client';

try {
  const receipt = await client.optionBook.fillOrder(order, 10_000000n);
} catch (error) {
  if (error instanceof OrderExpiredError) {
    // Order expired between fetch and fill — get a fresh list
    const freshOrders = await client.api.fetchOrders();
    // retry with freshOrders[0]
  } else if (error instanceof InsufficientAllowanceError) {
    // Approve first, then retry
    await client.erc20.ensureAllowance(
      client.chainConfig.tokens.USDC.address,
      client.chainConfig.contracts.optionBook,
      10_000000n,
    );
    const receipt = await client.optionBook.fillOrder(order, 10_000000n);
  } else if (error instanceof ContractRevertError) {
    console.error('Contract reverted:', error.message, error.cause);
  } else if (error instanceof ThetanutsError) {
    console.error(`SDK error [${error.code}]: ${error.message}`);
  }
}
```

## Production checklist

* Always call `previewFillOrder()` before `fillOrder()` — use `preview.totalCollateral` for the exact approval amount.
* Always call `ensureAllowance()` before `fillOrder()`. The SDK does not auto-approve.
* Check `order.order.expiry` before filling to avoid wasted gas on an already-expired order.
* Use a reliable RPC provider in production — the public `mainnet.base.org` endpoint has strict rate limits.

***

## See Also

* [Preview Fills](/sdk/optionbook/preview-fills)
* [Encode for External Wallets](/sdk/optionbook/encode-external-wallets)
* [Browse and Filter Orders](/sdk/optionbook/browse-filter-orders)


# Referrer Fees

Earn a share of trading fees by attaching your address as referrer on OptionBook order fills.

## How referrer fees work

1. The protocol owner whitelists your address and sets your fee split (e.g. `5000` bps = 50%).
2. When a taker fills an order with your referrer address, the protocol fee is calculated as `min(0.06% of notional, 12.5% of premium)`. Your share — `fee × feeBps / 10000` — accrues in the on-chain `fees[token][referrer]` ledger.
3. Fees accrue **per collateral token**: PUT fills accrue USDC fees, CALL fills accrue WETH or cbBTC fees.
4. You call `claimAllFees()` (or `claimFees(token)` for one token at a time) to withdraw.

**OptionFactory (RFQ) fees are separate.** RFQ fees use the `referralId` system and can only be withdrawn by the contract owner. Only OptionBook fees are self-claimable. See the [RFQ Referrals](/sdk/rfq-factory/referrals) guide for the OptionFactory side.

## Setting up a referrer

### Option 1: Client-level referrer (all fills use it automatically)

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  signer,
  referrer: '0x92b8ac05b63472d1D84b32bDFBBf3e1887331567',
});

// All fillOrder calls will use this referrer automatically
await client.optionBook.fillOrder(order, 10_000000n);
```

### Option 2: Per-fill referrer (overrides client default)

```typescript
await client.optionBook.fillOrder(
  order,
  10_000000n,
  '0xYourReferrerAddress', // overrides client.referrer for this call only
);
```

### Option 3: Encode methods (viem, wagmi, Account Abstraction wallets)

```typescript
const { to, data } = client.optionBook.encodeFillOrder(
  order,
  10_000000n,
  '0x92b8ac05b63472d1D84b32bDFBBf3e1887331567',
);
const hash = await walletClient.sendTransaction({ to, data });
```

If no referrer is provided, the zero address is used and no fees are earned.

## Querying fees

### Check your fee split

```typescript
const feeBps = await client.optionBook.getReferrerFeeSplit('0xYourAddress');
console.log(`Fee split: ${feeBps} bps (${Number(feeBps) / 100}%)`);
// e.g. 2500 bps = 25%
// 0 means not whitelisted — contact the protocol team
```

#### Setting splits (owner-only)

`setReferrerFeeSplit(referrer, feeBps)` configures the share of protocol fees a whitelisted referrer earns. The contract reverts for non-owner callers — only the OptionBook owner can call this. Third-party referrers must request whitelisting from the Thetanuts team.

```typescript
// Owner-only: reverts for everyone else.
await client.optionBook.setReferrerFeeSplit(referrer, 2500n);  // 25%
```

### Check a single token

```typescript
const usdc = client.chainConfig.tokens.USDC.address;
const feeAmount = await client.optionBook.getFees(usdc, '0xYourAddress');
console.log(`Accumulated USDC fees: ${ethers.formatUnits(feeAmount, 6)}`);
```

### Check all tokens at once

`getAllClaimableFees()` scans every configured collateral token in parallel and returns only non-zero balances. No signer required.

```typescript
const claimable = await client.optionBook.getAllClaimableFees('0xYourAddress');

if (claimable.length === 0) {
  console.log('No claimable fees.');
} else {
  for (const fee of claimable) {
    const formatted = ethers.formatUnits(fee.amount, fee.decimals);
    console.log(`${fee.symbol}: ${formatted}`);
  }
  // Example output:
  //   USDC: 12.50
  //   WETH: 0.003
}
```

**Parameters:** `address: string` — referrer address to check

**Returns:** `ClaimableFee[]` — array of `{ token, symbol, decimals, amount }` for each non-zero balance.

## Claiming fees

### Claim all tokens in one call

`claimAllFees()` finds claimable tokens, then claims each one sequentially. Partial failures are handled gracefully — if one token's claim fails, the rest still proceed.

```typescript
const results = await client.optionBook.claimAllFees();

for (const r of results) {
  if (r.receipt) {
    console.log(`Claimed ${r.symbol}: tx ${r.receipt.hash}`);
  } else {
    console.log(`Failed ${r.symbol}: ${r.error?.message}`);
    // Retry: await client.optionBook.claimFees(tokenAddress)
  }
}
```

**Parameters:** `address?: string` — referrer address. If omitted, uses the signer's address.

**Returns:** `ClaimFeeResult[]` — array of `{ symbol, amount, receipt?, error? }` for each token attempted.

**Throws:** `SIGNER_REQUIRED` if no signer is configured and no address provided.

### Claim a single token

```typescript
const usdc = client.chainConfig.tokens.USDC.address;
const receipt = await client.optionBook.claimFees(usdc);
console.log(`Claimed USDC fees: ${receipt.hash}`);
```

**Parameters:** `token: string` — collateral token address (USDC, WETH, cbBTC)

**Returns:** `TransactionReceipt`

## Full claim workflow example

See [`docs/examples/claim-fees.ts`](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/docs/examples/claim-fees.ts) for a complete, copy-paste ready example that:

1. Verifies your referrer whitelist status
2. Scans all tokens for claimable balances
3. Claims everything and reports results
4. Verifies zero balance after claiming

## End-to-end fee workflow summary

| Step                       | Method                         | Signer |
| -------------------------- | ------------------------------ | ------ |
| Check fee split            | `getReferrerFeeSplit(address)` | No     |
| Check single token balance | `getFees(token, address)`      | No     |
| Check all token balances   | `getAllClaimableFees(address)` | No     |
| Claim one token            | `claimFees(token)`             | Yes    |
| Claim all tokens           | `claimAllFees(address?)`       | Yes    |

***

## See Also

* [Fill Orders](/sdk/optionbook/fill-orders)
* [Encode for External Wallets](/sdk/optionbook/encode-external-wallets)
* [Overview](/sdk/optionbook/overview)


# Encode for External Wallets

Build raw transaction payloads for OptionBook fills without using the built-in ethers signer — for use with viem, wagmi, Account Abstraction wallets, Safe, and similar tools.

## When to use encode methods

Use `encodeFillOrder()` and `encodeSwapAndFillOrder()` instead of `fillOrder()` / `swapAndFillOrder()` when:

* You are using **viem or wagmi** and want to send the transaction through your own wallet client
* Your wallet is an **Account Abstraction wallet** (Coinbase Smart Wallet, Safe, Biconomy, etc.)
* You need **custom gas estimation or nonce management**
* You are building a **transaction batch** and need the raw calldata

Both encode methods return `{ to: string; data: string }` — the target contract address and encoded calldata. No transaction is sent.

## encodeFillOrder()

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const client = new ThetanutsClient({ chainId: 8453, provider });

// Fetch the order (no signer needed for this part)
const orders = await client.api.fetchOrders();
const order = orders[0];

// Encode the fill (no signer needed)
const { to, data } = client.optionBook.encodeFillOrder(
  order,
  10_000000n,                              // 10 USDC (optional — omit for max fill)
  '0x92b8ac05b63472d1D84b32bDFBBf3e1887331567', // optional referrer
);
```

**Parameters:** same as `fillOrder()`.

**Returns:** `{ to: string; data: string }`

## Sending with viem/wagmi

```typescript
import { createWalletClient, http } from 'viem';
import { base } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(),
});

// 1. Encode the fill
const { to, data } = client.optionBook.encodeFillOrder(order, 10_000000n);

// 2. Send via viem wallet client
const hash = await walletClient.sendTransaction({
  to: to as `0x${string}`,
  data: data as `0x${string}`,
});
console.log(`Transaction hash: ${hash}`);
```

## Sending with ethers.js signer directly

```typescript
const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

const { to, data } = client.optionBook.encodeFillOrder(order, 10_000000n);
const tx = await signer.sendTransaction({ to, data });
console.log(`Transaction hash: ${tx.hash}`);
```

## encodeSwapAndFillOrder()

Encoded variant of `swapAndFillOrder()`. Atomically swaps from a source token and fills the order — useful when the user holds WETH but the order requires USDC collateral.

```typescript
const { to, data } = client.optionBook.encodeSwapAndFillOrder(
  order,
  swapRouterAddress,       // e.g. 1inch, 0x, Odos router
  swapSrcTokenAddress,     // token the user is holding
  swapSrcAmount,           // amount of source token to swap (bigint)
  swapCalldata,            // pre-encoded swap calldata from your aggregator
  '0xYourReferrerAddress', // optional referrer
);

// Send via any wallet
const hash = await walletClient.sendTransaction({
  to: to as `0x${string}`,
  data: data as `0x${string}`,
});
```

**Parameters:** same as `swapAndFillOrder()`.

**Returns:** `{ to: string; data: string }`

You must approve the swap router to spend the source token before sending this transaction.

## Approval encoding for external wallets

If you also need to encode the collateral approval for an external wallet:

```typescript
const { to: approveTo, data: approveData } = client.erc20.encodeApprove(
  client.chainConfig.tokens.USDC.address,
  client.chainConfig.contracts.optionBook,
  10_000000n,
);

// Send approve tx first
const approveTx = await walletClient.sendTransaction({
  to: approveTo as `0x${string}`,
  data: approveData as `0x${string}`,
});

// Then send the fill tx
const { to, data } = client.optionBook.encodeFillOrder(order, 10_000000n);
const fillTx = await walletClient.sendTransaction({
  to: to as `0x${string}`,
  data: data as `0x${string}`,
});
```

## Referrer in encode methods

Encode methods accept the same referrer parameter as their non-encode counterparts:

```typescript
// No referrer (zero address used)
client.optionBook.encodeFillOrder(order, 10_000000n)

// With referrer
client.optionBook.encodeFillOrder(order, 10_000000n, '0xReferrerAddress')
```

***

## See Also

* [Fill Orders](/sdk/optionbook/fill-orders)
* [Referrer Fees](/sdk/optionbook/referrer-fees)
* [Overview](/sdk/optionbook/overview)


# Overview

Create custom options through a sealed-bid auction — choose any strike, expiry, and structure, then let market makers compete to fill your request.

## What is RFQ / Factory?

The RFQ (Request for Quotation) system, also called the **Factory**, lets you create fully custom options that are not listed on the OptionBook. You specify exactly what you want — underlying asset, strike(s), expiry, direction, and structure — and market makers respond with encrypted, competitive offers. The best offer wins and an option contract is deployed atomically at settlement.

Under the hood the OptionFactory contract acts as both auctioneer and deployer: it collects encrypted MM offers, runs the reveal auction, and mints a fresh EIP-1167 proxy option contract (or transfers an existing one) when the trade settles.

## Key Features

* **Privacy via sealed-bid auction** — Offers are encrypted with ECDH until the reveal phase. No MM can see a competitor's bid, preventing front-running and last-second sniping.
* **Competitive pricing** — Multiple market makers respond to each RFQ; the best price (lowest for BUY, highest for SELL) wins.
* **Full collateralization** — Every option created through the factory is 100% collateralized. Collateral is pulled at settlement, not at RFQ creation.
* **Atomic settlement** — Option deployment and collateral/premium transfer happen in a single transaction.
* **Multi-leg structures** — Vanilla, spreads (2-leg), butterflies (3-leg), condors and iron condors (4-leg), plus physically settled variants.
* **Cash-settled by default, physical optional** — RFQs are cash-settled by default via `buildRFQRequest()`. Cash settlement pays out the price difference in the collateral token (USDC for PUTs, WETH/cbBTC for CALLs). For physically settled options (actual delivery of underlying at expiry), use `buildPhysicalOptionRFQ()` instead (vanilla only).

## OptionBook vs RFQ (Factory)

|                 | **OptionBook**                                                               | **RFQ (Factory)**                                                                                 |
| --------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **What**        | Fill existing market-maker orders                                            | Create custom options via sealed-bid auction                                                      |
| **When to use** | Quick trades on listed options (vanilla and multi-leg)                       | Custom options via sealed-bid auction: any strike, any expiry, cash-settled or physically settled |
| **Structures**  | Vanilla, spread, butterfly, condor, iron condor (cash-settled)               | Vanilla, spread, butterfly, condor, iron condor (cash-settled or physically settled for vanilla)  |
| **Key methods** | `fillOrder()`, `previewFillOrder()`                                          | `buildRFQRequest()`, `requestForQuotation()`                                                      |
| **Pricing**     | Order prices from `fetchOrders()`                                            | MM pricing from `getAllPricing()`                                                                 |
| **Data source** | Book indexer (`/api/v1/book/`)                                               | Factory indexer (`/api/v1/factory/`)                                                              |
| **User data**   | `getUserPositionsFromIndexer()`                                              | `getUserRfqs()`, `getUserOptionsFromRfq()`                                                        |
| **Collateral**  | Paid upfront by taker                                                        | `collateralAmount = 0` (held by factory)                                                          |
| **Settlement**  | Cash-settled (payout in USDC/WETH/cbBTC based on price difference at expiry) | Cash-settled by default; physically settled optional via `buildPhysicalOptionRFQ()`               |

## When to Use RFQ

RFQ uses the same cash-settled implementation contracts as OptionBook (PUT, INVERSE\_CALL, spreads, etc.). The difference is that you choose the parameters instead of filling an existing order.

Choose the RFQ system when you need:

* A **cash-settled option with custom parameters** ... any strike price, any expiry date, any structure ... that isn't currently listed on the OptionBook (use `buildRFQRequest()`)
* **Price competition** ... you want multiple MMs to submit sealed bids rather than taking a single listed price
* A specific **multi-leg structure** (spread, butterfly, condor, iron condor) at your chosen strikes and expiry
* A **physically settled** option where actual tokens are delivered at expiry (use `buildPhysicalOptionRFQ()`, vanilla only)
* To **close an existing position** by specifying `existingOptionAddress`

Choose OptionBook when an existing maker order already matches what you want. OptionBook is faster (instant fill vs \~60 second auction).

## Quick Example

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });
const userAddress = await signer.getAddress();

// 1. Generate ECDH keypair (used to decrypt MM offers)
const keyPair = await client.rfqKeys.getOrCreateKeyPair();

// 2. Create a cash-settled PUT RFQ
const rfqRequest = client.optionFactory.buildRFQRequest({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  strike: 2000,
  expiry: Math.floor(Date.now() / 1000) + 86400 * 7,
  numContracts: 1,
  isLong: true,
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
  requesterPublicKey: keyPair.compressedPublicKey,
});

// 3. Send the RFQ transaction
const { to, data } = client.optionFactory.encodeRequestForQuotation(rfqRequest);
const tx = await signer.sendTransaction({ to, data });
console.log('RFQ created:', tx.hash);

// 4. Wait for MM offers, then settle (see Early Settlement or RFQ Lifecycle)
```

## High-Level Flow

```
1. You call requestForQuotation() with your ECDH public key
        ↓
2. Market makers see the on-chain event and submit encrypted offers
        ↓
3a. Early settlement: you decrypt an offer and accept it immediately
        — OR —
3b. Offer period ends → MMs reveal their amounts → anyone calls settleQuotation()
        ↓
4. OptionFactory deploys option contract, transfers collateral and premium atomically
        ↓
5. Option expires → oracle provides price → payout distributed
```

## Supported Structures

| Strikes | Structure   | Settlement | Implementation                   | SDK Method                                    |
| ------- | ----------- | ---------- | -------------------------------- | --------------------------------------------- |
| 1       | Vanilla     | Cash       | `PUT` / `INVERSE_CALL`           | `buildRFQRequest()`                           |
| 1       | Vanilla     | Physical   | `PHYSICAL_PUT` / `PHYSICAL_CALL` | `buildPhysicalOptionRFQ()`                    |
| 2       | Spread      | Cash       | `PUT_SPREAD` / `CALL_SPREAD`     | `buildRFQRequest()` or `buildSpreadRFQ()`     |
| 3       | Butterfly   | Cash       | `PUT_FLY` / `CALL_FLY`           | `buildRFQRequest()` or `buildButterflyRFQ()`  |
| 4       | Condor      | Cash       | `PUT_CONDOR` / `CALL_CONDOR`     | `buildRFQRequest()` or `buildCondorRFQ()`     |
| 4       | Iron Condor | Cash       | `IRON_CONDOR`                    | `buildRFQRequest()` or `buildIronCondorRFQ()` |

The SDK detects the structure automatically from the length of the `strikes` array passed to `buildRFQParams()` or `buildRFQRequest()`.

***

## Advanced: Swap and Create in One Tx

`client.optionFactory.swapAndCall(params)` lets a user swap a source token through a DEX aggregator and then call any OptionFactory function (most commonly `requestForQuotation`) in a single atomic transaction. Useful when the user holds the wrong token for collateral and doesn't want to send two separate transactions.

```typescript
await client.optionFactory.swapAndCall({
  swapRouter,        // aggregator router address
  swapSrcToken,      // token the user is paying with
  swapDstToken,      // token expected as collateral / escrow
  swapSrcAmount,     // amount of source token to swap
  swapCallData,      // encoded swap calldata from the aggregator
  selfCallData,      // encoded call to OptionFactory (e.g. requestForQuotation)
});
```

The aggregator's calldata must be quoted client-side; the SDK passes it through unchanged. The CLI wraps this whole flow behind `rfq request --pay-with` — see [Paying with a Different Token](/sdk/rfq-factory/pay-with).

Two constraints are easy to miss:

* **The factory executes the swap, not the caller.** Approve the `OptionFactory` (not the router) for `swapSrcToken`, and build the aggregator route with `sender` and `recipient` both set to the factory. A route built for the user's wallet delivers the output to the wrong address and reverts.
* **Native ETH only reaches WETH.** Pass `swapRouter: ZeroAddress` with the ETH in `msg.value` and the factory wraps it 1:1 — `swapSrcAmount` is not read on that path. There is no native-ETH swap: supplying `msg.value` alongside a router reverts `NativeTokenNotAllowedForSwap`. To fund non-WETH collateral from ETH, wrap to WETH first and swap from there.

***

## See Also

* [Create an RFQ](/sdk/rfq-factory/create-rfq) — Step-by-step guide to submitting your first RFQ
* [Paying with a Different Token](/sdk/rfq-factory/pay-with) — Fund collateral from an asset you already hold
* [RFQ Lifecycle](/sdk/rfq-factory/lifecycle) — Detailed phases, collateral handling, and settlement paths
* [Multi-Leg Structures](/sdk/rfq-factory/multi-leg-structures) — Spreads, butterflies, and condors with examples


# Create an RFQ

Submit a Request for Quotation to the OptionFactory and have market makers compete to fill your custom option. This page covers **cash-settled** RFQs (the default). For physically settled options, see [Physical Options](/sdk/rfq-factory/physical-options).

RFQ uses the same cash-settled implementation contracts as OptionBook (PUT, INVERSE\_CALL, spreads, etc.). The difference is that you choose the strike and expiry instead of filling an existing order.

## Prerequisites

* Install the SDK: `npm install @thetanuts-finance/thetanuts-client`
* A funded wallet on Base mainnet (chain ID 8453)
* For **SELL positions**: USDC (or WETH/cbBTC) approved for the OptionFactory
* For **BUY positions**: enough of the collateral token to cover the escrowed `reservePrice` — or, in the CLI, fund it from another asset in the same transaction with [`--pay-with`](/sdk/rfq-factory/pay-with)

***

## Critical Rule: collateralAmount is ALWAYS 0

When creating any RFQ, the `collateralAmount` parameter must always be `0`. Collateral is **not** locked at RFQ creation — it is pulled from the seller at settlement time.

```typescript
// CORRECT — buildRFQParams enforces this automatically
const params = client.optionFactory.buildRFQParams({ ... });
// params.collateralAmount === BigInt(0)  always

// WRONG — never set manually
collateralAmount: BigInt(1000000)  // causes issues at settlement!
```

***

## Approach 1: buildRFQRequest (One-Liner)

`buildRFQRequest` combines `buildRFQParams` and the tracking/key fields into a single object ready for `encodeRequestForQuotation`.

### BUY Position (isLong: true)

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });
const userAddress = await signer.getAddress();

// 1. Generate or load your ECDH keypair (used to decrypt MM offers)
const keyPair = await client.rfqKeys.getOrCreateKeyPair();

// 2. Build the complete RFQ request
const rfqRequest = client.optionFactory.buildRFQRequest({
  requester: userAddress,
  underlying: 'ETH',              // 'ETH' | 'BTC'
  optionType: 'PUT',              // 'CALL' | 'PUT'
  strike: 2000,                   // human-readable strike price
  expiry: Math.floor(Date.now() / 1000) + 86400 * 7,  // 7 days from now
  numContracts: 1.5,              // human-readable contract count
  isLong: true,                   // true = BUY, false = SELL
  offerDeadlineMinutes: 60,       // how long MMs have to respond
  collateralToken: 'USDC',        // 'USDC' | 'WETH' | 'cbBTC'
  reservePrice: 0.015,            // optional: max price per contract (BUY)
  requesterPublicKey: keyPair.compressedPublicKey,
});

// 3. Encode and send
const { to, data } = client.optionFactory.encodeRequestForQuotation(rfqRequest);
const tx = await signer.sendTransaction({ to, data });
console.log('RFQ created:', tx.hash);
```

### SELL Position (isLong: false) — Approval Required

When selling (going short), **you are the collateral provider**. You must approve the OptionFactory to pull collateral at settlement before creating the RFQ.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });
const userAddress = await signer.getAddress();

const keyPair = await client.rfqKeys.getOrCreateKeyPair();

// Collateral approval amounts:
//   PUT:           strike × numContracts  (in USDC decimals = 6)
//   CALL (inverse): numContracts          (in WETH decimals = 18)
const strike = 1850;
const numContracts = 1.5;
const approvalAmount = BigInt(Math.round(strike * numContracts * 1e6)); // USDC

const USDC = client.chainConfig.tokens.USDC.address;
await client.erc20.ensureAllowance(
  USDC,
  client.optionFactory.contractAddress,
  approvalAmount
);

// Build and submit SELL RFQ
const rfqRequest = client.optionFactory.buildRFQRequest({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  strike: 1850,
  expiry: Math.floor(Date.now() / 1000) + 86400 * 7,
  numContracts: 1.5,
  isLong: false,                  // SELL — you provide collateral
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
  reservePrice: 0.010,            // optional: min acceptable price (SELL)
  requesterPublicKey: keyPair.compressedPublicKey,
});

const { to, data } = client.optionFactory.encodeRequestForQuotation(rfqRequest);
const tx = await signer.sendTransaction({ to, data });
console.log('SELL RFQ created:', tx.hash);
```

***

## Approach 2: buildRFQParams (Two-Step)

`buildRFQParams` builds just the on-chain `QuotationParameters` object. Use this when you want fine-grained control over the tracking fields or reserve price encoding.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });
const userAddress = await signer.getAddress();

const keyPair = await client.rfqKeys.getOrCreateKeyPair();

// Step 1: Build the QuotationParameters object
const quotationParams = client.optionFactory.buildRFQParams({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  strike: 1850,           // human-readable; SDK converts to 8-decimal bigint
  expiry: 1741334400,     // unix timestamp
  numContracts: 1.5,      // human-readable; SDK converts to token-decimal bigint
  isLong: true,
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
});
// quotationParams.collateralAmount === 0n  — enforced automatically

// Step 2: Encode the full request with reserve price and public key
const { to, data } = client.optionFactory.encodeRequestForQuotation({
  params: quotationParams,
  tracking: { referralId: BigInt(0), eventCode: BigInt(0) },
  reservePrice: BigInt(0),  // 0 = no price protection; or set in collateral decimals
  requesterPublicKey: keyPair.compressedPublicKey,
});

const tx = await signer.sendTransaction({ to, data });
console.log('RFQ created:', tx.hash);
```

***

## Reserve Price

The `reservePrice` parameter protects you from unfavorable fills:

| Position               | Meaning                                    | Behavior if violated        |
| ---------------------- | ------------------------------------------ | --------------------------- |
| BUY (`isLong: true`)   | Maximum price per contract you will pay    | RFQ fails; deposit returned |
| SELL (`isLong: false`) | Minimum price per contract you will accept | RFQ fails                   |
| omitted or `0`         | No price protection                        | Accept any offer            |

**Example:** BUY with reserve price 0.015 USDC, 10 contracts → total escrow = 0.015 × 10 = **0.15 USDC** locked until settlement.

In `buildRFQRequest`, pass `reservePrice` as a human-readable number (e.g. `0.015`). When using `buildRFQParams` + `encodeRequestForQuotation` directly, pass it as a bigint in collateral token decimals.

***

## RFQ Parameters Reference

| Parameter              | Type                          | Description                                                                                                                                   |
| ---------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `requester`            | `address`                     | Your wallet address                                                                                                                           |
| `underlying`           | `'ETH' \| 'BTC'`              | Underlying asset                                                                                                                              |
| `optionType`           | `'CALL' \| 'PUT'`             | Option type                                                                                                                                   |
| `strike`               | `number`                      | Strike price (human-readable)                                                                                                                 |
| `strikes`              | `number[]`                    | Strike array for multi-leg. The generic SDK builder selects structure from option type plus strike count; MCP callers use explicit `product`. |
| `expiry`               | `number`                      | Unix timestamp; must be after offer deadline                                                                                                  |
| `numContracts`         | `number \| bigint \| string`  | Contract count; use `bigint` when closing positions                                                                                           |
| `isLong`               | `boolean`                     | `true` = BUY, `false` = SELL                                                                                                                  |
| `offerDeadlineMinutes` | `number`                      | Minutes MMs have to respond                                                                                                                   |
| `collateralToken`      | `'USDC' \| 'WETH' \| 'cbBTC'` | Collateral denomination                                                                                                                       |
| `reservePrice`         | `number`                      | Optional max (BUY) or min (SELL) price per contract                                                                                           |
| `requesterPublicKey`   | `hex string`                  | Your ECDH compressed public key                                                                                                               |

For MCP callers, `prepare_request_rfq` validates the explicit `product` against the strike count before calldata is built: `PUT`/`CALL` require 1 strike, `*_SPREAD` require 2, `*_FLY` require 3, and `*_CONDOR`/`IRON_CONDOR` require 4. BUY RFQs require a positive per-contract `reservePrice`; use `prepare_suggest_reserve_price` instead of guessing.

***

## Convenience Helpers: Multi-Leg Builders

The factory module exposes named helpers that make parameter intent explicit:

```typescript
// Spread: pass named lower/upper strikes
const spreadRFQ = client.optionFactory.buildSpreadRFQ({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  lowerStrike: 1800,
  upperStrike: 2000,
  expiry: 1741334400,
  numContracts: 1,
  isLong: true,
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
});

// Butterfly: pass lower, middle, upper
const butterflyRFQ = client.optionFactory.buildButterflyRFQ({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'CALL',
  lowerStrike: 1800,
  middleStrike: 1900,
  upperStrike: 2000,
  expiry: 1741334400,
  numContracts: 1,
  isLong: true,
  offerDeadlineMinutes: 60,
  collateralToken: 'WETH',
});

// Condor: pass four ordered strikes
const condorRFQ = client.optionFactory.buildCondorRFQ({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  strike1: 1700,
  strike2: 1800,
  strike3: 1900,
  strike4: 2000,
  expiry: 1741334400,
  numContracts: 1,
  isLong: true,
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
});
```

These return the same shape as `buildRFQRequest` and can be passed directly to `encodeRequestForQuotation` or `requestForQuotation`.

***

## Using requestForQuotation (Convenience)

Instead of encoding and sending manually, the module also provides a transaction-sending wrapper:

```typescript
const receipt = await client.optionFactory.requestForQuotation(rfqRequest);
console.log('TX hash:', receipt.hash);
console.log('Block:', receipt.blockNumber);

// Get the new RFQ ID
const rfqCount = await client.optionFactory.getQuotationCount();
const rfqId = rfqCount - 1n;
console.log('RFQ ID:', rfqId.toString());
```

***

## Common Pitfalls

| Mistake                                               | Fix                                               |
| ----------------------------------------------------- | ------------------------------------------------- |
| `collateralAmount != 0`                               | Always use `buildRFQParams`; never set manually   |
| SELL RFQ without token approval                       | Call `ensureAllowance` before submitting          |
| `expiry` before offer deadline                        | Ensure `expiry > now + offerDeadlineMinutes * 60` |
| Floating-point `numContracts` when closing a position | Pass the on-chain `bigint` value directly         |
| `reservePrice` in wrong decimals                      | Use `buildRFQRequest` with human-readable number  |

***

## See Also

* [RFQ Lifecycle](/sdk/rfq-factory/lifecycle) — What happens after you submit: MM offers, reveal phase, settlement
* [Multi-Leg Structures](/sdk/rfq-factory/multi-leg-structures) — Spreads, butterflies, condors with collateral math
* [Key Management](/sdk/rfq-factory/key-management) — How ECDH keys are stored and used for offer decryption


# Paying with a Different Token

An RFQ's collateral token is decided by the structure you trade, not by you. If you don't hold that token, `--pay-with` funds it from something you *do* hold — in the same transaction, through `OptionFactory.swapAndCall`.

> Available in the CLI (`thetanuts rfq request --pay-with`). SDK users can build the same call with `client.optionFactory.encodeSwapAndCall()`; see [Overview](/sdk/rfq-factory/overview#advanced-swap-and-create-in-one-tx).

## BUY requests only

`--pay-with` applies to `--direction BUY` (a long request), which escrows `reservePrice` at the moment it is submitted. That escrow is what the swap funds.

A `--direction SELL` request escrows **nothing** when it is submitted — the factory pulls collateral from the seller at *settlement*, after a maker fills — so there is nothing to fund up front and the CLI refuses the combination. Approve the collateral token before settlement instead (see [Create an RFQ](/sdk/rfq-factory/create-rfq)), or acquire it separately.

## Which collateral does my structure use?

| Structure                                                           | Collateral      | Notes                                                                                                                            |
| ------------------------------------------------------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Single-strike ETH CALL                                              | **WETH**        | This is `INVERSE_CALL`. The CLI requires `--collateral-token WETH` explicitly, and `--collateral-amount` is denominated in WETH. |
| Puts, call spreads, put spreads, butterflies, condors, iron condors | **USDC**        | Passing `--collateral-token WETH` is rejected for these.                                                                         |
| BTC calls                                                           | not yet exposed | Would require cbBTC collateral.                                                                                                  |

The collateral row in `rfq build` / `rfq request` output shows the symbol alongside the address, so you can confirm before submitting.

## What can I pay with?

| Your product's collateral  | You can pay with                                                                                                                                                                                    | You cannot pay with                                                               |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| **WETH** (ETH calls)       | <p><code>eth</code> — wrapped 1:1 inside the factory, no approval, no aggregator<br><code>usdc</code>, <code>cbbtc</code>, <code>cbdoge</code>, <code>cbxrp</code>, or any routable Base ERC-20</p> | `weth` — it is already the collateral                                             |
| **USDC** (everything else) | `weth`, `cbbtc`, `cbdoge`, `cbxrp`, or any routable Base ERC-20                                                                                                                                     | <p><code>eth</code> — see below<br><code>usdc</code> — already the collateral</p> |

### Native ETH only ever becomes WETH

`swapAndCall` has two mutually exclusive paths, and neither takes native ETH to a token other than WETH:

* **Wrap path** (`swapRouter = address(0)`): the factory calls `swapDstToken.call{value: msg.value}("")`. That only succeeds against a contract with a payable `receive()` — i.e. WETH. A USDC destination reverts `WrapperTransferFailed`.
* **Swap path** (`swapRouter != address(0)`): sending any `msg.value` reverts `NativeTokenNotAllowedForSwap`.

So there is no ETH → USDC route. To fund a USDC-collateral product from ETH, wrap to WETH first and use `--pay-with weth`.

## Examples

```bash
# Hold USDC, want an ETH call (WETH collateral) — swap happens in the same tx
thetanuts rfq request \
  --underlying ETH --type CALL --strike 4000 --expiry 1787904000 \
  --collateral-token WETH --direction BUY --contracts 0.1 \
  --pay-with usdc --pay-amount 500

# Hold native ETH, same product — wraps 1:1, no approval, no aggregator
thetanuts rfq request ... --collateral-token WETH --pay-with eth

# Hold WETH, want a USDC-collateral put spread
thetanuts rfq request ... --pay-with weth --pay-amount 0.2
```

`--expiry` is a Unix timestamp in **seconds**, and the strike/expiry pair must exist in the market maker's live quote grid — run `thetanuts rfq quote --underlying ETH --type call` to see what is currently tradeable.

Add `--dry-run` to any of these to see the quote, the approval target, and the calldata without broadcasting.

## Sizing

* `--pay-amount` is **required** for an ERC-20: the CLI cannot quote a route without knowing how much you intend to spend.
* `--pay-amount` is **optional** for `eth`: the wrap is exactly 1:1, so it is sized automatically from the required deposit.
* **Excess is refunded** by the contract, so erring high is safe.

The required deposit is the request's `reservePrice` — the maximum total the factory escrows from a buyer. If the quote cannot cover it after slippage, the CLI refuses before you sign and suggests a larger `--pay-amount` rather than letting the transaction revert on-chain. Take that suggestion rather than computing an amount yourself; excess is refunded either way.

### "Reserve price of 0"

`reservePrice` is `contracts x premium per contract`, and `--pay-with` refuses a request whose reserve is 0 — there is no escrow for a swap to fund.

This catches people out because of **how the trade was sized**, not because of anything to do with `--pay-with`:

* `--collateral-amount <n>` fetches the market maker's live ask and fills the reserve in for you.
* `--contracts <n>` does **not**. It leaves `reservePrice` at 0 unless you also pass `--reserve-price <premium per contract>`.

Submitting without `--pay-with` is not the workaround: a zero-reserve request offers makers nothing and will not be filled.

## Safety rails

| Flag                         | Default    | Behaviour                                                                                                |
| ---------------------------- | ---------- | -------------------------------------------------------------------------------------------------------- |
| `--slippage-bps <n>`         | `100` (1%) | Aggregator slippage tolerance. Values above 500 bps require `--force-slippage`.                          |
| `--max-price-impact-bps <n>` | `200` (2%) | Refuses to broadcast above this price impact.                                                            |
| `--force-slippage`           | off        | Accepts impact/slippage above the caps, and accepts a route the aggregator returned without USD pricing. |

Every one of these is evaluated while the plan is built — *before* any approval transaction is broadcast — so a rejected route costs you no gas.

Additionally, and without any flag:

* Your wallet's balance of the pay-with token (or of ETH, on the wrap path) is checked up front, so a shortfall is a readable error rather than an opaque transfer revert after the approval has already mined.
* The router is checked against `OptionFactory.authorizedRouters` **on-chain** before you are asked to sign.
* The route is re-quoted at the broadcast boundary, never carried through the confirmation prompt, and both the price-impact and minimum-output checks re-run against the fresh quote.
* **The minimum you are shown is the minimum that is enforced.** Re-quoting refreshes the route's internals and its deadline; it can never lower the floor you approved. If the refreshed route guarantees less, nothing is broadcast and the CLI tells you to quote again. A 0.5% re-quote allowance is already priced into the `minReceived` figure at the prompt, so ordinary second-to-second price movement does not abort the run.
* **The aggregator's executable calldata is decoded before you sign**, and the trade encoded in it is checked against the one you were quoted: the tokens spent and bought, the amount, the destination receiver, and the minimum the router will enforce on-chain. That decoded minimum — not the plaintext figure the API reports alongside it — is what the floor above is compared against. A route carrying a router-level fee, or one using an entrypoint the CLI cannot decode, is refused rather than signed.
* A route with no USD pricing is refused rather than treated as zero impact.
* The configured chain is asserted against the RPC before the approval and the swap are sent.

## Approve the OptionFactory, not the router

The **factory** executes the swap, not your wallet. So the ERC-20 approval goes to the `OptionFactory` address, and the aggregator route is built with `sender` and `recipient` both set to the factory. The CLI handles this for you and prints the approval target explicitly in `--dry-run` output. If you are building the call yourself with the SDK, approving the router instead of the factory is the single most common way to mis-wire this flow.

`--pay-with` cannot be combined with `--ensure-allowance`: the latter targets the collateral token, which is not what you spend on this path.

## Limitations

* BUY (`--direction BUY`) requests only — see [above](#buy-requests-only).
* Base (chainId 8453) only. ERC-20 `--pay-with` is unavailable on other chains.
* `rfq request` only. `book fill` does not support swapping yet — see [Fill Orders](/sdk/optionbook/fill-orders#swapandfillorder) for the contract-level equivalent.
* The pay-with token needs a KyberSwap route on Base. Aave receipt tokens (`aBasWETH`, `aBascbBTC`, `aBasUSDC`) are in chain config but are unlikely to route.
* Cannot be used to close an existing option: `existingOptionAddress` must be zero, or the contract reverts `SwapAndRFQNotAllowedForExistingOptions`.

## See Also

* [Create an RFQ](/sdk/rfq-factory/create-rfq)
* [RFQ Overview](/sdk/rfq-factory/overview)
* [RFQ Lifecycle](/sdk/rfq-factory/lifecycle)


# Multi-Leg Structures

Build spreads, butterflies, and condors through the RFQ system using the SDK's multi-leg helpers.

## How the SDK Detects Structure

The structure is determined entirely by the number of strikes you pass. You do not need to specify the implementation address manually.

```
strikes.length === 1  →  VANILLA    (single option)
strikes.length === 2  →  SPREAD     (2-leg)
strikes.length === 3  →  BUTTERFLY  (3-leg)
strikes.length === 4  →  CONDOR     (4-leg)
```

Pass strikes to `buildRFQParams` or `buildRFQRequest` as a `strikes` array (or use the named helpers). The SDK auto-sorts strikes for the correct implementation, selects the right contract implementation, and calculates collateral.

> **Settlement types:** All multi-leg structures are **cash-settled only**. Physical delivery is available only for vanilla (single-strike) options.

***

## All Option Structures Summary

| Structure   | Strikes | Implementation               | Strike Order                                     | Collateral Formula             |
| ----------- | ------- | ---------------------------- | ------------------------------------------------ | ------------------------------ |
| Vanilla     | 1       | `PUT` / `INVERSE_CALL`       | N/A                                              | PUT: `strike × N` / CALL: `N`  |
| Spread      | 2       | `PUT_SPREAD` / `CALL_SPREAD` | PUT: desc, CALL: asc                             | `(upper − lower) × N`          |
| Butterfly   | 3       | `PUT_FLY` / `CALL_FLY`       | PUT: desc, CALL: asc                             | `(middle − lower) × N`         |
| Condor      | 4       | `PUT_CONDOR` / `CALL_CONDOR` | Always ascending                                 | `(strike2 − strike1) × N`      |
| Iron Condor | 4       | `IRON_CONDOR`                | `[strike1, strike2, strike3, strike4]` ascending | `max(putWidth, callWidth) × N` |

***

## Spread (2 Legs)

A spread limits both max profit and max loss compared to a vanilla option.

```
PUT SPREAD                         CALL SPREAD
(Buy high put, sell low put)       (Buy low call, sell high call)

     ╱────                              ────╲
    ╱                                       ╲
───╱                                         ╲───
$1700  $1900                           $1800  $2000

Collateral: (1900 − 1700) × N = $200 × N
```

### buildRFQParams with strikes array

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });
const userAddress = await signer.getAddress();
const keyPair = await client.rfqKeys.getOrCreateKeyPair();

// PUT SPREAD: 2 strikes — SDK auto-sorts to descending for PUT
const spreadRequest = client.optionFactory.buildRFQRequest({
  requester: userAddress as `0x${string}`,
  underlying: 'ETH',
  optionType: 'PUT',
  strikes: [1800, 2000],    // 2 values = SPREAD; auto-sorted to [2000, 1800]
  expiry: 1741334400,
  numContracts: 1,
  isLong: false,            // SELL — you provide collateral
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
  reservePrice: 0.001,
  requesterPublicKey: keyPair.compressedPublicKey,
});

// Collateral for SELL spread: (2000 − 1800) × 1 = 200 USDC
await client.erc20.ensureAllowance(
  client.chainConfig.tokens.USDC.address,
  client.optionFactory.contractAddress,
  200_000000n  // 200 USDC in 6 decimals
);

const receipt = await client.optionFactory.requestForQuotation(spreadRequest);
console.log('Spread RFQ TX:', receipt.hash);
```

### buildSpreadRFQ (named helper)

```typescript
const spreadRFQ = client.optionFactory.buildSpreadRFQ({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  lowerStrike: 1800,
  upperStrike: 2000,
  expiry: 1741334400,
  numContracts: 1,
  isLong: false,
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
});
```

***

## Butterfly (3 Legs)

A butterfly profits most when the underlying expires near the middle strike, with limited risk on either side.

```
PUT BUTTERFLY                      CALL BUTTERFLY

     ╱╲                                 ╱╲
    ╱  ╲                               ╱  ╲
───╱    ╲───                       ───╱    ╲───
$1700 $1800 $1900                  $1800 $1900 $2000

Legs: +1@lower, −2@middle, +1@upper
Collateral: (middle − lower) × N = $100 × N
```

### Full example with real TX hash

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });
const userAddress = await signer.getAddress();
const keyPair = await client.rfqKeys.getOrCreateKeyPair();

// PUT BUTTERFLY: 3 strikes
const butterflyRequest = client.optionFactory.buildRFQRequest({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  strikes: [1700, 1800, 1900],  // 3 values = BUTTERFLY
  expiry: 1741334400,
  numContracts: 0.001,
  isLong: false,                // SELL (short) butterfly
  offerDeadlineMinutes: 6,
  collateralToken: 'USDC',
  reservePrice: 0.0001,
  requesterPublicKey: keyPair.compressedPublicKey,
});

// Collateral = (middle − lower) × numContracts = (1800 − 1700) × 0.001 = 0.1 USDC
await client.erc20.ensureAllowance(
  client.chainConfig.tokens.USDC.address,
  client.optionFactory.contractAddress,
  100000n  // 0.1 USDC in 6 decimals
);

const receipt = await client.optionFactory.requestForQuotation(butterflyRequest);
console.log('Butterfly RFQ TX:', receipt.hash);
```

**Real example (RFQ 784 — PUT BUTTERFLY):**

* Structure: $1700 / $1800 / $1900 PUT BUTTERFLY
* Offer deadline: 04:10:34 UTC
* MM offer: 0.000223 USDC at 04:05:45 UTC
* Early settlement: 04:07:09 UTC (3 min before deadline)
* TX: `0x105f75cdfb64a3796100f6d667bc4f7fec3836d2b5aa5c43b66073a1b40964ee`

### buildButterflyRFQ (named helper)

```typescript
const butterflyRFQ = client.optionFactory.buildButterflyRFQ({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'CALL',
  lowerStrike: 1800,
  middleStrike: 1900,
  upperStrike: 2000,
  expiry: 1741334400,
  numContracts: 1,
  isLong: true,
  offerDeadlineMinutes: 60,
  collateralToken: 'WETH',
});
```

***

## Condor (4 Legs)

A condor provides a profit zone between the two middle strikes, combining two spreads.

```
PUT CONDOR                         CALL CONDOR

   ╱────╲                               ╱────╲
  ╱      ╲                             ╱      ╲
─╱        ╲─                         ─╱        ╲─
$1600 $1700 $1800 $1900         $1800 $1900 $2000 $2100

Legs: +1@s1, −1@s2, −1@s3, +1@s4
Collateral: (strike2 − strike1) × N = $100 × N
```

### Full example with real TX hash

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });
const userAddress = await signer.getAddress();
const keyPair = await client.rfqKeys.getOrCreateKeyPair();

// PUT CONDOR: 4 strikes
const condorRequest = client.optionFactory.buildRFQRequest({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  strikes: [1600, 1700, 1800, 1900],  // 4 values = CONDOR
  expiry: 1741334400,
  numContracts: 0.001,
  isLong: false,                       // SELL (short) condor
  offerDeadlineMinutes: 6,
  collateralToken: 'USDC',
  reservePrice: 0.0001,
  requesterPublicKey: keyPair.compressedPublicKey,
});

// Collateral = (strike2 − strike1) × numContracts = (1700 − 1600) × 0.001 = 0.1 USDC
await client.erc20.ensureAllowance(
  client.chainConfig.tokens.USDC.address,
  client.optionFactory.contractAddress,
  100000n  // 0.1 USDC in 6 decimals
);

const receipt = await client.optionFactory.requestForQuotation(condorRequest);
console.log('Condor RFQ TX:', receipt.hash);
```

**Real example (RFQ 785 — PUT CONDOR):**

* Structure: $1600 / $1700 / $1800 / $1900 PUT CONDOR
* MM offer: 0.003248 USDC
* Early settlement: 04:15:00 UTC (4 min before deadline)
* TX: `0xa89fb6dbad43b430399bbdec878927185e602b7df9b5390f71d2d11c33e4d850`
* Option deployed: `0x20D51d70A51Aa529eb9460a49aAC94910A1bc267`

### buildCondorRFQ (named helper)

```typescript
const condorRFQ = client.optionFactory.buildCondorRFQ({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  strike1: 1600,
  strike2: 1700,
  strike3: 1800,
  strike4: 1900,
  expiry: 1741334400,
  numContracts: 1,
  isLong: false,
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
});
```

***

## Iron Condor (4 Legs, Mixed)

An iron condor combines a put spread and a call spread with different underlying directions, straddling a range.

```typescript
// buildIronCondorRFQ uses strike1-4 in ascending order: [putLower, putUpper, callLower, callUpper]
const ironCondorRFQ = client.optionFactory.buildIronCondorRFQ({
  requester: userAddress,
  underlying: 'ETH',
  strike1: 1700,   // put spread lower leg
  strike2: 1800,   // put spread upper leg
  strike3: 2000,   // call spread lower leg
  strike4: 2100,   // call spread upper leg
  expiry: 1741334400,
  numContracts: 1,
  isLong: false,
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
});
// Collateral = max(putWidth, callWidth) × N = max(100, 100) × 1 = 100 USDC
```

***

## Collateral by Structure

| Structure    | Collateral Formula             | Example (N=1)                  |
| ------------ | ------------------------------ | ------------------------------ |
| Vanilla PUT  | `strike × N`                   | $2000 × 1 = 2000 USDC          |
| Vanilla CALL | `N` (underlying)               | 1 × 1 = 1 WETH                 |
| Spread       | `(upper − lower) × N`          | ($2000 − $1800) × 1 = 200 USDC |
| Butterfly    | `(middle − lower) × N`         | ($1900 − $1800) × 1 = 100 USDC |
| Condor       | `(strike2 − strike1) × N`      | ($1700 − $1600) × 1 = 100 USDC |
| Iron Condor  | `max(putWidth, callWidth) × N` | max(100, 100) × 1 = 100 USDC   |

***

## Strike Ordering

The SDK automatically sorts strikes in the order the contract requires. You can pass them in any order.

```
PUT structures:    sorted DESCENDING  (e.g. [2000, 1800])
CALL structures:   sorted ASCENDING   (e.g. [1800, 2000])
CONDOR:            always ASCENDING
IRON_CONDOR:       [putLower, putUpper, callLower, callUpper]
```

***

## Product Type Reference

For collateral formulas and max loss calculations, see the [Collateral Cost Reference](/sdk/pricing/collateral-cost).

***

## See Also

* [Create an RFQ](/sdk/rfq-factory/create-rfq) — Single-leg vanilla RFQ walkthrough
* [RFQ Lifecycle](/sdk/rfq-factory/lifecycle) — Offer period, reveal phase, and settlement
* [Early Settlement](/sdk/rfq-factory/early-settlement) — Accept a multi-leg offer before the deadline


# RFQ Lifecycle

Complete walkthrough of the four phases of an RFQ — from creation through settlement and expiry.

***

## High-Level Timeline

```
┌─────────────────┬─────────────────┬─────────────────┐
│  OFFER PERIOD   │  REVEAL PERIOD  │   SETTLEMENT    │
│                 │                 │                 │
│ MMs submit      │ MMs reveal      │ Option created  │
│ encrypted       │ actual offer    │ or transferred  │
│ offers          │ amounts         │                 │
└─────────────────┴─────────────────┴─────────────────┘
     offerEndTimestamp    + REVEAL_WINDOW
```

***

## User Flow Diagram

```
┌─────────────────────────────────────────────────────────────────┐
│ 1. USER CREATES RFQ                                             │
│    - Select: underlying, strike(s), expiry, type                │
│    - Choose direction: BUY (long) or SELL (short)               │
│    - Set offer deadline (e.g., 60 minutes)                      │
│    - Optionally set reserve price (max/min acceptable)          │
│    - Generate ECDH keypair for encrypted offers                 │
│                                                                 │
│    For BUY: Deposit reserve price (max you'll pay)              │
│    For SELL: Approve collateral tokens                          │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. MARKET MAKERS RESPOND (during offer period)                  │
│    - MM sees RFQ event on-chain                                 │
│    - MM calculates price from exchange data                     │
│    - MM creates EIP-712 signed offer                            │
│    - MM encrypts offer with ECDH (only requester can decrypt)   │
│    - MM submits signature on-chain (amount NOT revealed)        │
└────────────────────────┬────────────────────────────────────────┘
                         │
          ┌──────────────┴──────────────┐
          │                             │
          ▼                             ▼
┌─────────────────────┐    ┌─────────────────────────────────────┐
│ EARLY SETTLEMENT    │    │ 3. REVEAL PHASE                     │
│ (Optional)          │    │    - After offer period ends        │
│                     │    │    - MMs reveal actual amounts      │
│ User decrypts offer │    │    - Best offer wins                │
│ and calls           │    │    - Losers get refunds             │
│ settleQuotationEarly│    │                                     │
└─────────┬───────────┘    └─────────────────┬───────────────────┘
          │                                  │
          └──────────────┬───────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. SETTLEMENT                                                   │
│                                                                 │
│    For NEW options:                                             │
│    - Clone option implementation (EIP-1167 proxy)              │
│    - Set buyer and seller addresses                             │
│    - Transfer collateral to option contract                     │
│    - Transfer premium to seller (minus fees)                    │
│                                                                 │
│    For EXISTING options:                                        │
│    - Transfer position ownership                                │
│    - Handle collateral/premium exchange                         │
│                                                                 │
│    Fees: 0.06% of notional, capped at 12.5% of premium          │
└────────────────────────┬────────────────────────────────────────┘
                         │
                         ▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. OPTION EXPIRY                                                │
│                                                                 │
│    - Oracle provides settlement price                           │
│    - Option contract calculates payout                          │
│    - If ITM: Buyer receives payout from collateral              │
│    - If OTM: Seller gets full collateral back                   │
└─────────────────────────────────────────────────────────────────┘
```

***

## Phase 1: User Creates RFQ

### What You Provide

| Parameter              | Description                                      | Example                        |
| ---------------------- | ------------------------------------------------ | ------------------------------ |
| `underlying`           | Asset (ETH or BTC)                               | `'ETH'`                        |
| `optionType`           | CALL or PUT                                      | `'PUT'`                        |
| `strike` / `strikes`   | Strike price(s)                                  | `1850` or `[1700, 1800, 1900]` |
| `expiry`               | Expiry unix 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 per contract (optional) | `0.015`                        |

### Example: Create a Vanilla PUT RFQ

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });
const userAddress = await signer.getAddress();
const keyPair = await client.rfqKeys.getOrCreateKeyPair();

const rfqRequest = client.optionFactory.buildRFQRequest({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  strike: 2000,
  expiry: Math.floor(Date.now() / 1000) + 86400 * 7,  // 7 days
  numContracts: 1.5,
  isLong: true,
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
  reservePrice: 0.015,
  requesterPublicKey: keyPair.compressedPublicKey,
});

const { to, data } = client.optionFactory.encodeRequestForQuotation(rfqRequest);
const tx = await signer.sendTransaction({ to, data });
console.log('RFQ created:', tx.hash);
```

### Critical: collateralAmount is ALWAYS 0

`collateralAmount` in the on-chain parameters must always be `0`. Collateral is NOT locked at RFQ creation — it is pulled from the seller at settlement. The `buildRFQParams` helper enforces this automatically.

### For BUY Positions (isLong: true)

* User deposits `reservePrice × numContracts` as escrow (maximum total payment)
* No collateral needed from user (MM provides collateral as the seller)
* Factory holds the deposit until settlement

### For SELL Positions (isLong: false)

* User must **approve** collateral tokens for the OptionFactory before submitting
* Approval amount:
  * **PUT**: `strike × numContracts` (in USDC decimals)
  * **CALL (inverse)**: `numContracts` (in WETH/cbBTC decimals)
  * **Spreads/Butterflies/Condors**: `(upperStrike − lowerStrike) × numContracts` (width × contracts)
* Collateral is 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 factor in both the market price and the opportunity cost of locking up collateral:

```
Base Price = Exchange bid/ask price

Collateral Cost = collateral_amount × APR × time_to_expiry
  - cbBTC: 1% APR
  - WETH:  4% APR
  - USDC:  7% APR

For SELLING to user (MM is short):
  Total = Base Ask + Collateral Cost

For BUYING from user (MM is long):
  Total = Base Bid − Collateral Cost
```

### What Is Stored On-Chain

During the offer period, **only the signature** is stored on-chain. The actual offer amount is:

* Encrypted with ECDH (only the requester can decrypt)
* Not stored on-chain (privacy)
* Revealed in Phase 3

### MM-Side SDK Calls

Three SDK methods cover the MM side of the offer flow. All require a signer.

```typescript
// 1. Submit an encrypted offer during the offer period.
//    The SDK does the EIP-712 signing and ECDH encryption for you.
await client.optionFactory.makeOfferForQuotation({
  quotationId,
  offerAmount: ethers.parseUnits('25', 6),  // raw bid in token units
  requesterPublicKey,                        // pulled from QuotationRequested event
  // Optional: pass a pre-generated nonce. SDK auto-generates one if omitted.
});

// 2. Cancel an outstanding offer before the reveal period ends.
//    Refunds any deposit the MM posted.
await client.optionFactory.cancelOfferForQuotation(quotationId);

// 3. Reveal the offer once the offer period closes.
//    Must match the signature submitted in step 1.
await client.optionFactory.revealOffer({
  quotationId,
  offerAmount,   // same value as in makeOfferForQuotation
  nonce,         // same nonce
});
```

The reveal pairs with the keypair flow described in [Key Management](/sdk/rfq-factory/key-management). MMs that miss the reveal window forfeit any chance of winning the RFQ even if they had the best offer.

***

## Phase 3: Reveal Phase

After `offerEndTimestamp`, market makers call `revealOffer()` with their actual amounts. The factory validates each reveal against the stored signature and selects the best offer:

```
For BUY RFQs (user buying):
  - First offer: always accepted as current best
  - Subsequent offers: must be LOWER than current best
  - Winner: lowest offer (cheapest for buyer)

For SELL RFQs (user selling):
  - First offer: always accepted as current best
  - Subsequent offers: must be HIGHER than current best
  - Winner: highest offer (most valuable for seller)
```

Losing offers get their deposits refunded during this phase.

***

## Settlement Paths: Early vs Normal

### Early Settlement

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

```
Timeline:
├── RFQ Created
├── MM submits encrypted offer
├── Requester decrypts offer (using ECDH private key)
├── Requester calls settleQuotationEarly()    ← BEFORE offer period ends
└── Option created immediately
```

**When to use:** You see a good offer and want to lock it in before the deadline.

**Who can call:** Only the requester (permissioned).

```typescript
// Decrypt an MM's offer and settle early
const quotationId = 784n;
const offerEvents = await client.events.getOfferMadeEvents({
  quotationId,
  fromBlock: currentBlock - 1000,
});

const offer = offerEvents[0];
const keyPair = await client.rfqKeys.loadKeyPair();
const decrypted = await client.rfqKeys.decryptOffer(
  offer.signedOfferForRequester,
  offer.signingKey,
);

console.log('Offer amount:', ethers.formatUnits(decrypted.offerAmount, 6), 'USDC');

// Accept the offer
const { to, data } = client.optionFactory.encodeSettleQuotationEarly(
  quotationId,
  decrypted.offerAmount,
  decrypted.nonce,
  offer.offeror,
);
const tx = await signer.sendTransaction({ to, data });
console.log('Early settlement TX:', tx.hash);
```

See [Early Settlement](/sdk/rfq-factory/early-settlement) for a complete code example.

### Normal Settlement

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

```
Timeline:
├── RFQ Created
├── MMs submit encrypted offers
├── Offer period ends (offerEndTimestamp)
├── REVEAL PERIOD begins
├── MMs call revealOffer() with actual amounts + nonces
├── Anyone calls settleQuotation()    ← AFTER reveal period
└── Option created with best offer
```

**When to use:** Let all MMs compete; auction determines the best price automatically.

**Who can call:** Anyone — settlement is permissionless after the reveal period.

```typescript
// After reveal period, no decryption needed
const { to, data } = client.optionFactory.encodeSettleQuotation({ quotationId });
const tx = await signer.sendTransaction({ to, data });
await tx.wait();
// — or use the convenience method —
await client.optionFactory.settleQuotation(quotationId);
```

### Why Is Normal Settlement Permissionless?

After the reveal period all offers are public on-chain. The winning offer is deterministic (best price wins), so no special permissions are needed to execute. In practice, **MM bots often settle their own winning RFQs** because they want to lock in the trade immediately.

### Settlement Paths Comparison

|              | Early Settlement          | Normal Settlement            |
| ------------ | ------------------------- | ---------------------------- |
| When         | During offer period       | After reveal period ends     |
| Who can call | Requester only            | Anyone (permissionless)      |
| Who decrypts | User (via ECDH)           | MM reveals on-chain          |
| Speed        | Immediate                 | Must wait for deadlines      |
| Competition  | Accept one specific offer | Best offer from all MMs wins |

***

## Phase 4: Settlement — New Option Creation

When `existingOptionAddress == address(0)` (creating a new option):

1. **Clone contract**: Factory deploys option using EIP-1167 minimal proxy
2. **Initialize**: Set buyer, seller, strikes, expiry, collateral token
3. **Transfer collateral**: Pull from seller, deposit into option contract
4. **Pay premium**: Transfer premium from buyer to seller, minus protocol fees
5. **Return excess**: Refund any unused escrow deposit to requester

### Existing Option Transfer

When `existingOptionAddress != address(0)` (closing/transferring an existing position):

1. **Validate state**: Option not yet settled, parameters match exactly
2. **Transfer ownership**: Change buyer/seller addresses
3. **Handle collateral**: Return or re-lock collateral as applicable
4. **Exchange premium**: Transfer between parties

### Fee Structure

```
Fee = min(collateral_fee, max_fee)

Where:
  collateral_fee = numContracts × 6 × price / 1e8 / 10000   (0.06%)
  max_fee        = premium × 125 / 1000                      (12.5% of premium)

Distribution:
  With referral:    50% to referrer, 50% to protocol
  Without referral: 100% to protocol
```

***

## Collateral Handling

### Key Principle: SELLER Always Provides Collateral

| RFQ Type               | Who Is Seller  | Who Provides Collateral |
| ---------------------- | -------------- | ----------------------- |
| BUY (`isLong: true`)   | Market Maker   | Market Maker            |
| SELL (`isLong: false`) | User/Requester | User/Requester          |

### Where Collateral Lives

1. **Before settlement**: In the seller's wallet (approved for OptionFactory)
2. **At settlement**: OptionFactory pulls it and transfers to the Option Contract
3. **During option life**: Locked in the Option Contract (not OptionFactory)
4. **At expiry**: 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        |
| Option active | Collateral 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          |
| ----------------------- | --------------- | ------------------------ |
| Expires OTM (worthless) | 0               | Full collateral returned |
| Expires ITM (partially) | Intrinsic value | Collateral minus payout  |
| Maximally exercised     | Full collateral | 0                        |

### Fund Flow Diagram

```
AT SETTLEMENT:

  Seller ─── Collateral ───> OptionFactory ─── Collateral ───> Option Contract
                                   │
  Buyer ──── Premium ────>         │
                                   └──── Premium − Fees ────> Seller

AT EXPIRY:

  If ITM:
       Option Contract ─── Payout ────> Buyer
       Option Contract ─── Remaining ─> Seller

  If OTM:
       Option Contract ─── Full Collateral ────> Seller
```

### Collateral Amount by Option Type

| Option Type    | Collateral Formula                   | Example                        |
| -------------- | ------------------------------------ | ------------------------------ |
| CALL (inverse) | `numContracts` (WETH/cbBTC)          | 1 WETH per contract            |
| PUT            | `strike × numContracts / 10^8`       | $1850 × 1 = 1850 USDC          |
| Spread         | `(upper − lower) × numContracts`     | ($2000 − $1800) × 1 = 200 USDC |
| Butterfly      | `(middle − lower) × numContracts`    | ($1900 − $1800) × 1 = 100 USDC |
| Condor         | `(strike2 − strike1) × numContracts` | ($1700 − $1600) × 1 = 100 USDC |

***

## Sealed-Bid Auction Mechanism

### Why Sealed Bids?

Traditional auctions suffer from:

* **Front-running**: Bots see bids and outbid by the minimum increment
* **Sniping**: Waiting until the last second to bid
* **Collusion**: MMs can coordinate if they see each other's prices

Sealed-bid auction prevents all three:

* Offers are encrypted until the reveal phase
* Only the requester can decrypt (ECDH)
* No MM knows competitors' bids during the offer period

### ECDH Encryption Flow

```
1. Requester generates keypair:
   - private key (kept secret, stored in .thetanuts-keys/)
   - public key (shared in RFQ on-chain)

2. MM generates ephemeral keypair per offer:
   - mm_private_key (kept secret)
   - mm_public_key (shared with offer)

3. Both compute the same shared secret:
   Requester: shared = ECDH(requester_private, mm_public)
   MM:        shared = ECDH(mm_private, requester_public)

4. MM encrypts offer:
   encrypted = AES-256-GCM(shared_secret, { nonce, offerAmount })

5. Only requester can decrypt:
   - Has requester_private_key
   - Computes same shared secret
   - Decrypts offer details
```

***

## Sequence of Events

```
    User                OptionFactory           Market Maker         Option Contract
      │                      │                       │                     │
      │  1. Create RFQ       │                       │                     │
      │─────────────────────>│                       │                     │
      │                      │  2. RFQ Event         │                     │
      │                      │──────────────────────>│                     │
      │                      │  3. Encrypted Offer   │                     │
      │                      │<──────────────────────│                     │
      │  4a. Early Settle    │                       │                     │
      │─────────────────────>│                       │                     │
      │         OR           │                       │                     │
      │                      │  4b. Reveal Offer     │                     │
      │                      │<──────────────────────│                     │
      │  4b. Normal Settle   │                       │                     │
      │─────────────────────>│                       │                     │
      │                      │  5. Deploy Option     │                     │
      │                      │────────────────────────────────────────────>│
      │                      │  6. Transfer Collateral                     │
      │                      │────────────────────────────────────────────>│
      │                      │  7. Pay Premium       │                     │
      │                      │──────────────────────>│                     │
      │                      │        ... time passes until expiry ...     │
      │  8. Claim Payout     │                       │                     │
      │───────────────────────────────────────────────────────────────────>│
      │<───────────────────────────────────────────────────────────────────│
```

***

## 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/MM/anyone | 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       |

***

## Timing Constraints

| Action            | When                                                 |
| ----------------- | ---------------------------------------------------- |
| Make offer        | Before `offerEndTimestamp`                           |
| Early settlement  | Before `offerEndTimestamp` (requester only)          |
| Reveal offer      | After `offerEndTimestamp`, before reveal period ends |
| Normal settlement | After reveal period ends (anyone)                    |
| Cancel RFQ        | Any time (requester only)                            |
| Cancel offer      | Before reveal period ends (MM only)                  |

***

## See Also

* [Create an RFQ](/sdk/rfq-factory/create-rfq) — Submitting a BUY or SELL request
* [Early Settlement](/sdk/rfq-factory/early-settlement) — Decrypt and accept an offer before the deadline
* [Key Management](/sdk/rfq-factory/key-management) — ECDH keypair storage and encryption details


# Early Settlement

Accept a market maker's offer before the offer period ends by decrypting it with your ECDH private key and calling `settleQuotationEarly`.

## When to Use Early Settlement

Early settlement lets the requester skip the reveal auction and lock in a specific offer immediately. Use it when:

* You see a good offer and don't want to risk it being outbid or withdrawn
* You need the option contract deployed as quickly as possible
* You are testing or iterating and want deterministic settlement timing

If you want the auction to run its course and let all MMs compete, use normal settlement instead (see [RFQ Lifecycle — Normal Settlement](/sdk/rfq-factory/lifecycle#normal-settlement)).

## Settlement Paths Comparison

|                | Early Settlement          | Normal Settlement                      |
| -------------- | ------------------------- | -------------------------------------- |
| When           | During offer period       | After reveal period ends               |
| Who can call   | Requester only            | Anyone (permissionless)                |
| Who decrypts   | User (ECDH)               | MM reveals on-chain                    |
| Speed          | Immediate                 | Must wait for offer + reveal deadlines |
| Competition    | Accept one specific offer | Best offer from all MMs wins           |
| Typical caller | Requester                 | Often MM auto-settle bots              |

***

## Full Example: Decrypt and Settle Early

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

const quotationId = 784n;  // RFQ ID from requestForQuotation

// ─────────────────────────────────────────────────
// Step 1: Find the MM's offer from on-chain events
// ─────────────────────────────────────────────────
const currentBlock = await provider.getBlockNumber();
const offerEvents = await client.events.getOfferMadeEvents({
  quotationId,
  fromBlock: currentBlock - 1000,
});

if (offerEvents.length === 0) {
  throw new Error('No offers found yet — MMs may not have responded');
}

const offer = offerEvents[0];
console.log('MM address:', offer.offeror);
console.log('MM public key:', offer.signingKey);

// ─────────────────────────────────────────────────
// Step 2: Load the ECDH keypair used when creating the RFQ
//         Must be the same key — not a newly generated one
// ─────────────────────────────────────────────────
const keyPair = await client.rfqKeys.loadKeyPair();
// If you get KeyNotFoundError here, the key was lost or you're on a
// different machine. Create a new RFQ with a fresh key instead.

// ─────────────────────────────────────────────────
// Step 3: Decrypt the offer
// ─────────────────────────────────────────────────
const decrypted = await client.rfqKeys.decryptOffer(
  offer.signedOfferForRequester,  // encrypted blob from OfferMade event
  offer.signingKey                // MM's ephemeral public key
);

console.log('Offer amount:', ethers.formatUnits(decrypted.offerAmount, 6), 'USDC');
console.log('Nonce:', decrypted.nonce.toString());

// ─────────────────────────────────────────────────
// Step 4: Accept the offer (early settle)
// ─────────────────────────────────────────────────
// Option A: encode and send manually
const { to, data } = client.optionFactory.encodeSettleQuotationEarly(
  quotationId,
  decrypted.offerAmount,
  decrypted.nonce,
  offer.offeror
);
const tx = await signer.sendTransaction({ to, data });
await tx.wait();
console.log('Early settlement TX:', tx.hash);

// Option B: convenience method (sends the transaction for you)
const receipt = await client.optionFactory.settleQuotationEarly(
  quotationId,
  decrypted.offerAmount,
  decrypted.nonce,
  offer.offeror
);
console.log('Early settlement TX:', receipt.hash);
```

***

## Real Example: RFQ 784 — PUT Butterfly Early Settlement

* **Structure:** $1700 / $1800 / $1900 PUT BUTTERFLY
* **Offer deadline:** 04:10:34 UTC
* **MM offer submitted:** 04:05:45 UTC (0.000223 USDC)
* **Early settled:** 04:07:09 UTC (3 minutes before deadline)
* **Settlement TX:** `0x105f75cdfb64a3796100f6d667bc4f7fec3836d2b5aa5c43b66073a1b40964ee`

The requester decrypted the offer, verified the price was acceptable (0.000223 USDC total), and called `settleQuotationEarly` — locking in the trade without waiting for the auction to complete.

***

## What Happens at Settlement

When `settleQuotationEarly` is called:

1. Factory verifies the decrypted `(offerAmount, nonce)` matches the stored MM signature
2. Factory validates the offer is within the requester's `reservePrice`
3. Factory deploys a new option contract (EIP-1167 proxy) or transfers an existing one
4. Collateral is pulled from the seller and deposited into the option contract
5. Premium is transferred to the seller (minus protocol fees)
6. Any unused escrow deposit is returned to the requester

***

## Cancellation

If no offer is acceptable, the requester can cancel the RFQ at any time:

```typescript
// Cancel the whole RFQ (requester only)
await client.optionFactory.cancelQuotation(quotationId);

// A MM can also cancel their own offer
await client.optionFactory.cancelOfferForQuotation(quotationId);
```

***

## Troubleshooting

### "No offers found"

The MM has not responded yet, or you searched too few blocks. Increase the `fromBlock` range or wait longer. Offer events are emitted as `OfferMade`.

### "KeyNotFoundError: RFQ key not found"

Your ECDH private key is not in storage. This happens if:

* You switched to a different machine
* The key directory (`.thetanuts-keys/`) was deleted
* You used `MemoryStorageProvider` and the process restarted

If the key is lost, you cannot decrypt this RFQ's offers. Create a new RFQ with `getOrCreateKeyPair()` and back up the key this time. See [Key Management](/sdk/rfq-factory/key-management).

### "DecryptionError: Authentication failed"

The ECDH shared secret is wrong. Verify that the keypair you loaded was the one used when creating the specific RFQ. The requester's public key is stored in the on-chain `QuotationParameters` — compare it with `keyPair.compressedPublicKey`.

```typescript
const quotation = await client.optionFactory.getQuotation(rfqId);
const stored = quotation.requesterPublicKey;
const local = (await client.rfqKeys.loadKeyPair()).compressedPublicKey;
console.log('Match:', stored === local);
```

***

## See Also

* [RFQ Lifecycle](/sdk/rfq-factory/lifecycle) — Full four-phase lifecycle including normal settlement
* [Key Management](/sdk/rfq-factory/key-management) — How ECDH keys are stored, backed up, and imported
* [Create an RFQ](/sdk/rfq-factory/create-rfq) — Submitting a BUY or SELL RFQ


# Key Management

Manage the ECDH keypairs used to encrypt and decrypt market maker offers in the RFQ system.

## Why Keys Matter

Every RFQ you create includes your ECDH compressed public key. Market makers use it to encrypt their offers so that only you can decrypt them (sealed-bid privacy). The corresponding private key must be available when you want to:

* **Decrypt an offer** for early settlement
* **Debug** a decryption failure by comparing stored vs on-chain public keys

If you lose your private key, you cannot decrypt any offers made against RFQs that used the corresponding public key. There is no recovery mechanism.

***

## Storage Providers

The SDK automatically selects a storage provider based on your runtime environment:

| Environment | Default Provider           | Persistence  | Location                                                                             |
| ----------- | -------------------------- | ------------ | ------------------------------------------------------------------------------------ |
| **Node.js** | internal file storage      | Persistent   | `.thetanuts-keys/` directory (permissions 0o600)                                     |
| **Browser** | explicit provider required | App-defined  | Use encrypted/passphrase-backed storage; plaintext `localStorage` is not the default |
| **Testing** | `MemoryStorageProvider`    | Lost on exit | In-memory only                                                                       |

***

## Automatic Key Management

The simplest usage: call `getOrCreateKeyPair()` and the SDK handles everything.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const client = new ThetanutsClient({ chainId: 8453, provider });

// Returns existing keypair from storage, or generates and stores a new one
const keyPair = await client.rfqKeys.getOrCreateKeyPair();
console.log('Public Key:', keyPair.compressedPublicKey);
// Keys are saved automatically and survive process restarts (Node.js)
```

***

## Custom Storage Provider

Node.js persists keys to `./.thetanuts-keys/` automatically. To use a different backend, pass any object that implements `KeyStorageProvider`:

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';
import type { KeyStorageProvider } from '@thetanuts-finance/thetanuts-client';

const customStorage: KeyStorageProvider = {
  async get(keyId) { return myStore.get(keyId); },
  async set(keyId, privateKey) { await myStore.set(keyId, privateKey); },
  async remove(keyId) { await myStore.delete(keyId); },
  async has(keyId) { return myStore.has(keyId); },
};

const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  keyStorageProvider: customStorage,
});

const keyPair = await client.rfqKeys.getOrCreateKeyPair();
```

***

## Memory Storage (Testing Only)

Use `MemoryStorageProvider` in tests or CI environments where you don't need persistence:

```typescript
import { ThetanutsClient, MemoryStorageProvider } from '@thetanuts-finance/thetanuts-client';

// WARNING: Keys are LOST when the process exits!
const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  keyStorageProvider: new MemoryStorageProvider(),
});
```

The SDK logs a warning when `MemoryStorageProvider` is used, as a reminder that the keys will not persist.

***

## Custom Storage Provider

Implement `KeyStorageProvider` to store keys in a database, cloud secret manager, or any other backend:

```typescript
import { KeyStorageProvider } from '@thetanuts-finance/thetanuts-client';

class MyDatabaseStorage implements KeyStorageProvider {
  async get(keyId: string): Promise<string | null> {
    return await myDatabase.get(keyId);
  }

  async set(keyId: string, privateKey: string): Promise<void> {
    await myDatabase.set(keyId, privateKey);
  }

  async remove(keyId: string): Promise<void> {
    await myDatabase.delete(keyId);
  }

  async has(keyId: string): Promise<boolean> {
    return await myDatabase.exists(keyId);
  }
}

const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  keyStorageProvider: new MyDatabaseStorage(),
});
```

***

## Key Backup Warning

> **CRITICAL**: Back up your RFQ private keys. If lost, you cannot decrypt offers made to your public key. There is no recovery mechanism.
>
> * **Node.js**: Keys are in `.thetanuts-keys/` with 0o600 permissions — back up this directory
> * **Browser**: pass an explicit `keyStorageProvider`; do not store production RFQ private keys in plaintext `localStorage`

### Export for Backup

```typescript
// Export private key (store this somewhere secure)
const privateKeyHex = await client.rfqKeys.exportPrivateKey();
console.log('Backup this key:', privateKeyHex);
```

### Import from Backup

```typescript
// Restore from a backed-up private key
const keyPair = await client.rfqKeys.importFromPrivateKey(
  privateKeyHex,
  true  // store = true saves it to the configured storage provider
);
console.log('Restored public key:', keyPair.compressedPublicKey);
```

***

## RFQKeyManagerModule Method Reference

| Method                                | Description                                        | Requires Signer |
| ------------------------------------- | -------------------------------------------------- | --------------- |
| `generateKeyPair()`                   | Generate a new ECDH keypair (not stored)           | No              |
| `getOrCreateKeyPair()`                | Get from storage, or generate and store a new one  | No              |
| `loadKeyPair()`                       | Load existing keypair from storage                 | No              |
| `hasStoredKey()`                      | Check whether a key exists in storage              | No              |
| `storeKeyPair(keypair)`               | Explicitly save a keypair to storage               | No              |
| `removeStoredKey()`                   | Delete the stored keypair                          | No              |
| `exportPrivateKey()`                  | Export private key hex string for backup           | No              |
| `importFromPrivateKey(key, store?)`   | Import a keypair from a private key hex            | No              |
| `encryptOffer(amount, nonce, pubKey)` | Encrypt an offer for a given public key            | No              |
| `decryptOffer(data, pubKey)`          | Decrypt an incoming offer using stored private key | No              |
| `generateNonce()`                     | Generate a random nonce                            | No              |
| `getPublicKeyFromPrivate(key)`        | Derive compressed public key from private key      | No              |
| `isValidPublicKey(key)`               | Validate a compressed public key format            | No              |
| `getStorageKeyId()`                   | Get the storage identifier for the current key     | No              |

***

## Encryption Technical Details

### ECDH Key Exchange

The SDK uses secp256k1 ECDH for secure key exchange between requester and market maker:

```
1. Requester generates keypair:
   - Private key: 32 random bytes
   - Public key: Compressed (33 bytes, 0x02/0x03 prefix)

2. Market Maker generates ephemeral keypair per offer:
   - New keypair for each offer (forward secrecy)

3. Shared secret computation:
   ECDH produces: 0x04 || x-coordinate (32 bytes) || y-coordinate (32 bytes)

4. AES key derivation:
   AES-256 key = x-coordinate (first 32 bytes after 0x04 prefix)
   Note: raw x-coordinate, NOT a SHA256 hash — matches MM bot behavior

5. Encryption:
   Algorithm: AES-256-GCM
   IV: 12 random bytes
   Plaintext: JSON { "offerAmount": "...", "nonce": "..." }
   Output: IV (12 bytes) + ciphertext + auth tag (16 bytes)
```

### Nonce Format

The `nonce` field in decrypted offers can be in two formats depending on the source:

| Source | Format             | Example                |
| ------ | ------------------ | ---------------------- |
| MM Bot | 16-char hex string | `"987563ef5fde9655"`   |
| SDK    | Decimal string     | `"391788778684598574"` |

The SDK automatically detects and handles both formats during decryption.

***

## Key Mismatch Prevention

Common mistakes that cause "Authentication failed" or "KeyNotFoundError":

1. **Regenerating keys between RFQ creation and decryption** — Always use `getOrCreateKeyPair()`, never `generateKeyPair()`, for production flows
2. **Running on a different machine** without copying `.thetanuts-keys/`
3. **Using `MemoryStorageProvider`** in a long-running process that restarts

Best practices:

* Use `getOrCreateKeyPair()` for all production RFQ flows
* Back up `.thetanuts-keys/` before deploying to new infrastructure
* Verify the stored key matches the on-chain key before attempting decryption:

```typescript
// Verify key matches the RFQ before decrypting
const keyPair = await client.rfqKeys.loadKeyPair();
const quotation = await client.optionFactory.getQuotation(rfqId);

if (keyPair.compressedPublicKey !== quotation.params.requesterPublicKey) {
  throw new Error('Key mismatch — cannot decrypt offers for this RFQ');
}
```

***

## Decryption Troubleshooting

### "KeyNotFoundError: RFQ key not found"

* No key in storage for the current `keyStorageProvider`
* Check that `.thetanuts-keys/` exists (Node.js)
* If lost: create a new RFQ with a freshly generated key; old RFQ offers cannot be recovered

### "DecryptionError: Invalid ciphertext"

* Wrong private key, or the encrypted offer data is truncated
* Verify `encryptedOffer` is complete (from the event log, not truncated)

### "DecryptionError: Authentication failed"

* AES-GCM auth tag verification failed — shared secret is wrong
* Confirm the stored keypair was used when creating the RFQ (compare public keys)

***

## See Also

* [Create an RFQ](/sdk/rfq-factory/create-rfq) — Where `getOrCreateKeyPair()` is used in context
* [Early Settlement](/sdk/rfq-factory/early-settlement) — Decrypting an offer to accept it before the deadline
* [RFQ Lifecycle](/sdk/rfq-factory/lifecycle) — How the sealed-bid auction uses ECDH encryption


# Referrals

Earn a share of RFQ fees by registering a referral and routing requests through it. The OptionFactory referral system is **separate** from the OptionBook referral flow — different mechanics, different claim path.

> **Quick distinction:** OptionBook referrals use a per-address whitelist set by the protocol owner, and you claim your fees yourself. RFQ referrals use a self-service `referralId` you mint via `registerReferral`, and only the protocol owner can withdraw the accrued fees. If you want self-claim, see the [OptionBook Referrer Fees](/sdk/optionbook/referrer-fees) guide instead.

## How RFQ referrals work

1. You call `registerReferral(QuotationParameters)` to mint a fresh `referralId`. The contract sets `referralOwner[id] = your address` and assigns the next available numeric ID.
2. To use the referral on a real RFQ, pass `tracking.referralId = id` when calling `requestForQuotation(...)`. Fees on settled quotations accrue to that ID.
3. You monitor accrual via `getReferralFees(id)` and ownership via `getReferralOwner(id)` — both are public views.
4. When the protocol owner runs `withdrawFees(token, [ids…])`, fees from your referralId are paid out. **Third parties cannot call `withdrawFees`** — the contract reverts. Coordinate with Thetanuts on the withdrawal cadence.

## Differences vs OptionBook referrals

|                   | OptionBook                                         | OptionFactory (RFQ)                                                 |
| ----------------- | -------------------------------------------------- | ------------------------------------------------------------------- |
| **Whitelist**     | Owner-only `setReferrerFeeSplit(addr, bps)`        | None — `registerReferral` is self-service                           |
| **Identifier**    | Your address                                       | A `referralId` (uint256) the contract assigns                       |
| **Refer a trade** | Pass `referrer` to `fillOrder`                     | Pass `tracking.referralId` to `requestForQuotation`                 |
| **Track accrual** | `getAllClaimableFees(addr)`                        | `getReferralFees(id)`                                               |
| **Claim**         | **You** call `claimFees(token)` / `claimAllFees()` | **Owner** calls `withdrawFees(token, [ids])` — third parties revert |
| **SDK module**    | `client.optionBook`                                | `client.optionFactory`                                              |

## Registering a referral

`registerReferral` takes the same `QuotationParameters` shape as `requestForQuotation` (see [Create an RFQ](/sdk/rfq-factory/create-rfq)) and returns a transaction receipt. The contract emits a `ReferralRegistered(referralId, referrer)` event from which you read your new ID.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

const chainConfig = client.chainConfig;

const receipt = await client.optionFactory.registerReferral({
  requester: await signer.getAddress(),
  existingOptionAddress: '0x0000000000000000000000000000000000000000',
  collateral: chainConfig.tokens.USDC.address,
  collateralPriceFeed: chainConfig.priceFeeds.ETH,
  implementation: chainConfig.implementations.PUT,
  strikes: [BigInt(2000) * BigInt(1e8)],
  numContracts: BigInt(10) * BigInt(1e6),
  requesterDeposit: BigInt(0),
  collateralAmount: BigInt(0),  // ALWAYS 0
  expiryTimestamp: BigInt(Math.floor(Date.now() / 1000) + 86400 * 7),
  offerEndTimestamp: BigInt(Math.floor(Date.now() / 1000) + 86400),
  isRequestingLongPosition: true,
  convertToLimitOrder: false,
  extraOptionData: '0x',
});

// Pull the new referralId from the ReferralRegistered event in the receipt logs.
// Or, simpler: query the on-chain quotationCount before vs after to derive it.
```

> **Zero-address guard (v0.2.1+):** if `params.implementation` resolves to `0x000…000`, the SDK throws `INVALID_PARAMS` before the transaction is built. The seven `PHYSICAL_*_SPREAD/FLY/CONDOR/IRON_CONDOR` slots are placeholders in r12. See the [v0.2.1 GitHub Release](https://github.com/Thetanuts-Finance/thetanuts-sdk/releases/tag/v0.2.1) for the safety-upgrade details.

### Reading the new referralId

The cleanest path is parsing the `ReferralRegistered` event from `receipt.logs` using the OptionFactory ABI. The event signature lives in the package — `import { OPTION_FACTORY_ABI } from '@thetanuts-finance/thetanuts-client'`. If you'd rather not parse logs, the contract assigns IDs sequentially, so a "before/after" read of the on-chain referral counter works as a fallback.

## Using a referralId on an RFQ

Once you have an ID, attach it to any `requestForQuotation` call via `tracking.referralId`:

```typescript
await client.optionFactory.requestForQuotation({
  params: { /* the QuotationParameters for the option */ },
  tracking: {
    referralId: 42n,    // your registered referralId
    eventCode: 0n,
  },
  reservePrice: 0n,
  requesterPublicKey,
});
```

For the full `requestForQuotation` walkthrough, see [Create an RFQ](/sdk/rfq-factory/create-rfq). The placeholder `referralId: BigInt(0)` shown there is the "no referral" value — replace it with your real ID to route fees to your accrual bucket.

## Reading referral state

One on-chain view, available without a signer:

```typescript
// What's accrued?
const fees = await client.optionFactory.getReferralFees(42n);
// returns bigint — accumulated fees for this referralId, in collateral token units
```

> The previous `getReferralOwner` helper was removed in the v4 r12 audit pass — the matching `referralOwner` view does not exist on the deployed OptionFactory. To resolve a referralId back to its owner, listen for `ReferralRegistered` events or use the indexer (`client.api.getFactoryReferrerStats()`).

For richer aggregated stats (volume, fee breakdowns, daily metrics) the off-chain indexer exposes `client.api.getFactoryReferrerStats()` — see the [Examples](/sdk/resources/examples) page for the indexer flow.

## Claiming fees (owner-only)

> **Important.** `withdrawFees(token, ids[])` requires the OptionFactory contract owner. Calls from any other address revert. As a third-party referrer, you **cannot self-claim** RFQ fees. Track accrual via `getReferralFees(id)` and coordinate the actual payout with Thetanuts.

The owner-side method signature is the same on the SDK:

```typescript
// Owner only — non-owner calls revert at the contract level
await client.optionFactory.withdrawFees(
  '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',  // token (e.g. USDC)
  [42n, 73n, 158n],                              // referralIds to sweep
);
```

The contract pays out in whatever token the referred quotation settled into. If you ran multiple RFQs that settled in different tokens (USDC, WETH, cbBTC), the owner has to call `withdrawFees` once per token.

### Why the owner gate?

RFQ fees are denominated in collateral that's escrowed during the auction lifecycle. Routing claims through the owner lets the contract enforce that only fees from *settled* quotations get paid out, and to the right token + amount. The trade-off is that third-party referrers don't have the OptionBook flow's "claim whenever you want" property.

## ExpiredReferralSwept event

When a referral's underlying RFQ expires without settling, the owner can sweep the dangling fees. The contract emits:

```solidity
event ExpiredReferralSwept(uint256 indexed referralId, address indexed token, uint256 amount);
```

Listen for this event if you want to reconcile your expected accrual against on-chain reality. Same access rule applies — only the owner can trigger the sweep.

## See also

* [OptionBook Referrer Fees](/sdk/optionbook/referrer-fees) — the other referral system, with self-claim
* [Create an RFQ](/sdk/rfq-factory/create-rfq) — the full `requestForQuotation` walkthrough where you'd attach your `referralId`
* [Modules Overview — `client.optionFactory`](/sdk/sdk-reference/modules-overview) — full OptionFactory module surface
* [v0.2.1 GitHub Release](https://github.com/Thetanuts-Finance/thetanuts-sdk/releases/tag/v0.2.1) — context on the zero-address guards and r12 changes that affect referral registration


# Physical Options

Create physically settled options through the RFQ system — at expiry, the underlying asset is delivered rather than cash being paid out.

## Overview

Physical options involve actual delivery of the underlying asset at expiry rather than a cash settlement based on price difference. They are only available for **vanilla (single-strike) options**. Multi-leg structures (spreads, butterflies, condors) are cash-settled only.

| Option            | Seller Posts              | At ITM Expiry — Buyer Delivers | At ITM Expiry — Buyer Receives |
| ----------------- | ------------------------- | ------------------------------ | ------------------------------ |
| **Physical PUT**  | USDC (strike × contracts) | WETH (contracts)               | USDC (strike × contracts)      |
| **Physical CALL** | WETH (contracts)          | USDC (strike × contracts)      | WETH (contracts)               |

***

## Creating a Physical PUT RFQ

A physical PUT means: "I want to sell WETH at `strike` price." If the option expires ITM, the buyer delivers WETH and receives USDC at the strike price.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });
const userAddress = await signer.getAddress();

const keyPair = await client.rfqKeys.getOrCreateKeyPair();

// Next Friday 8:00 UTC (Deribit-compatible expiry)
const now = new Date();
const daysUntilFriday = (5 - now.getUTCDay() + 7) % 7 || 7;
const nextFriday = new Date(Date.UTC(
  now.getUTCFullYear(), now.getUTCMonth(),
  now.getUTCDate() + daysUntilFriday,
  8, 0, 0
));
const nextFridayExpiry = Math.floor(nextFriday.getTime() / 1000);

// SELL Physical PUT: "I want to buy ETH at $2500"
// If ETH falls below $2500, you receive ETH at your target price
const physicalPutRFQ = client.optionFactory.buildPhysicalOptionRFQ({
  requester: userAddress as `0x${string}`,
  underlying: 'ETH',
  optionType: 'PUT',
  strike: 2500,
  expiry: nextFridayExpiry,
  numContracts: 0.1,
  isLong: false,                    // SELL — you post USDC collateral
  deliveryToken: client.chainConfig.tokens.WETH.address as `0x${string}`,
  collateralToken: 'USDC',          // Auto-inferred for PUT: USDC
  offerDeadlineMinutes: 6,
  reservePrice: 0.0001,
  requesterPublicKey: keyPair.compressedPublicKey,
});

// Verify the implementation is PHYSICAL_PUT (not regular PUT)
console.log('Implementation:', physicalPutRFQ.params.implementation);
// Should match: client.chainConfig.implementations.PHYSICAL_PUT

// extraOptionData contains the ABI-encoded delivery token address
// (non-empty '0x' distinguishes physical from cash-settled)
console.log('extraOptionData:', physicalPutRFQ.params.extraOptionData);

// For SELL: approve USDC collateral (strike × numContracts)
const collateral = BigInt(Math.round(2500 * 0.1 * 1e6)); // 250 USDC
await client.erc20.ensureAllowance(
  client.chainConfig.tokens.USDC.address,
  client.optionFactory.contractAddress,
  collateral
);

const receipt = await client.optionFactory.requestForQuotation(physicalPutRFQ);
console.log('Physical PUT RFQ TX:', receipt.hash);
```

***

## Creating a Physical CALL RFQ

A physical CALL means: "I want to sell WETH above `strike` price." If the option expires ITM, the seller delivers WETH and receives USDC at the strike price.

```typescript
// SELL Physical CALL: "I'm willing to sell ETH at $3000"
// If ETH rises above $3000, you deliver ETH and receive USDC at strike
const physicalCallRFQ = client.optionFactory.buildPhysicalOptionRFQ({
  requester: userAddress as `0x${string}`,
  underlying: 'ETH',
  optionType: 'CALL',
  strike: 3000,
  expiry: nextFridayExpiry,
  numContracts: 0.1,
  isLong: false,                    // SELL — you post WETH collateral
  deliveryToken: client.chainConfig.tokens.USDC.address as `0x${string}`,
  collateralToken: 'WETH',          // Auto-inferred for CALL: WETH
  offerDeadlineMinutes: 6,
  reservePrice: 0.0001,
  requesterPublicKey: keyPair.compressedPublicKey,
});

// For SELL CALL: approve WETH collateral (numContracts WETH)
const wethCollateral = BigInt(Math.round(0.1 * 1e18)); // 0.1 WETH
await client.erc20.ensureAllowance(
  client.chainConfig.tokens.WETH.address,
  client.optionFactory.contractAddress,
  wethCollateral
);

const receipt = await client.optionFactory.requestForQuotation(physicalCallRFQ);
console.log('Physical CALL RFQ TX:', receipt.hash);
```

***

## Calculation Functions

The SDK exports standalone helpers for physical option math:

```typescript
import {
  calculateDeliveryAmount,
  isPhysicalProduct,
  calculateNumContracts,
  calculateCollateralRequired,
} from '@thetanuts-finance/thetanuts-client';

// Check if a product type is physically settled
isPhysicalProduct('PHYSICAL_CALL');  // true
isPhysicalProduct('PHYSICAL_PUT');   // true
isPhysicalProduct('PUT');            // false
isPhysicalProduct('CALL_SPREAD');    // false

// PHYSICAL_CALL: seller posts WETH, buyer delivers USDC at expiry
const callDelivery = calculateDeliveryAmount(10, 'PHYSICAL_CALL', [2000]);
// { deliveryAmount: 20000, deliveryToken: 'USDC' }
// (10 contracts × $2000 strike = $20,000 USDC delivered by buyer)

// PHYSICAL_PUT: seller posts USDC, buyer delivers WETH at expiry
const putDelivery = calculateDeliveryAmount(10, 'PHYSICAL_PUT', [2000]);
// { deliveryAmount: 10, deliveryToken: 'WETH' }
// (10 contracts × 1 = 10 WETH delivered by buyer)

// For BTC underlying — delivery token becomes cbBTC
const btcDelivery = calculateDeliveryAmount(5, 'PHYSICAL_PUT', [50000], 'BTC');
// { deliveryAmount: 5, deliveryToken: 'cbBTC' }
```

***

## Physical Option Product Table

| Product                | Collateral (Seller Posts)    | Delivery (Buyer Delivers at ITM Expiry) |
| ---------------------- | ---------------------------- | --------------------------------------- |
| `PHYSICAL_CALL`        | `numContracts` WETH          | `strike × numContracts` USDC            |
| `PHYSICAL_PUT`         | `strike × numContracts` USDC | `numContracts` WETH                     |
| `PHYSICAL_CALL_SPREAD` | `width × numContracts` WETH  | `width × numContracts` USDC             |
| `PHYSICAL_PUT_SPREAD`  | `width × numContracts` USDC  | `width × numContracts` WETH             |

For BTC-underlying options, substitute `cbBTC` for `WETH`.

***

## Collateral vs Delivery

Physical options involve two distinct asset transfers:

| Concept        | Who                  | When          | What                               |
| -------------- | -------------------- | ------------- | ---------------------------------- |
| **Collateral** | Seller               | At settlement | Posted to secure the option payout |
| **Delivery**   | Buyer (if exercised) | At ITM expiry | Asset exchanged for collateral     |

* For **Physical PUT**: Seller posts USDC (collateral); if ITM, buyer delivers WETH and receives USDC
* For **Physical CALL**: Seller posts WETH (collateral); if ITM, buyer delivers USDC and receives WETH

If the option expires OTM, no delivery occurs and the seller's collateral is returned in full.

***

## Limitations

* Physical settlement is available for **vanilla (single-strike) options only**
* Multi-leg structures (spreads, butterflies, condors) are cash-settled
* The `deliveryToken` address must be passed in `buildPhysicalOptionRFQ` and is ABI-encoded into `extraOptionData`

***

## See Also

* [Create an RFQ](/sdk/rfq-factory/create-rfq) — General RFQ creation (cash-settled vanilla and multi-leg)
* [Multi-Leg Structures](/sdk/rfq-factory/multi-leg-structures) — Spreads, butterflies, condors (cash-settled only)
* [RFQ Lifecycle](/sdk/rfq-factory/lifecycle) — Full lifecycle from creation to expiry


# Overview

Borrow USDC against ETH or BTC collateral with no liquidation risk. At expiry, repay and reclaim your collateral, or walk away and keep the USDC.

## How It Works

The Loan module wraps the LoanCoordinator contract, which uses Thetanuts V4 physically-settled call options under the hood. The flow:

1. **Deposit collateral** (WETH or cbBTC) and specify a strike price and expiry
2. **Market makers compete** to fill your loan via a sealed-bid RFQ auction
3. **Receive USDC** minus borrowing costs (option premium + interest + protocol fee)
4. **At expiry**, choose to exercise (repay USDC, get collateral back) or walk away (keep USDC, forfeit collateral)

No margin calls. No liquidation. Your collateral is locked in a smart contract, not lent out.

***

## Quick Start

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

// 1. Browse available strike/expiry combinations
const groups = await client.loan.getStrikeOptions('ETH');
const firstOption = groups[0].options[0];

// 2. Calculate exact loan costs
const calc = client.loan.calculateLoan({
  depositAmount: '1.0',
  underlying: 'ETH',
  strike: firstOption.strike,
  expiryTimestamp: firstOption.expiry,
  askPrice: firstOption.askPrice,
  underlyingPrice: firstOption.underlyingPrice,
});

console.log(`Receive: ${calc.formatted.receive} USDC`);
console.log(`Repay:   ${calc.formatted.repay} USDC`);
console.log(`APR:     ${calc.formatted.apr}%`);

// 3. Request the loan
const result = await client.loan.requestLoan({
  underlying: 'ETH',
  collateralAmount: '1.0',
  strike: firstOption.strike,
  expiryTimestamp: firstOption.expiry,
  minSettlementAmount: calc.finalLoanAmount,
});

console.log(`Loan ID: ${result.quotationId}`);
```

***

## Method Overview

| Method                                       | Description                                         | Signer |
| -------------------------------------------- | --------------------------------------------------- | ------ |
| `requestLoan(params)`                        | Deposit collateral and request a loan               | Yes    |
| `acceptOffer(id, amount, nonce, offeror)`    | Accept an MM's offer (early settlement)             | Yes    |
| `cancelLoan(id)`                             | Cancel a pending loan request                       | Yes    |
| `exerciseOption(address)`                    | Repay USDC and reclaim collateral at expiry         | Yes    |
| `doNotExercise(address)`                     | Walk away at expiry (keep USDC, forfeit collateral) | Yes    |
| `swapAndExercise(address, aggregator, data)` | Swap collateral to USDC via DEX, then exercise      | Yes    |
| `lend(id)`                                   | Fill a borrower's limit order with USDC             | Yes    |
| `getLendingOpportunities(options?)`          | Fetch unfilled limit orders from indexer            | No     |
| `getLoanRequest(id)`                         | Query on-chain loan state                           | No     |
| `getUserLoans(address)`                      | Get all loans for an address from indexer           | No     |
| `getOptionInfo(address)`                     | Get option contract details                         | No     |
| `isOptionITM(address)`                       | Check if option is in-the-money                     | No     |
| `fetchPricing()`                             | Fetch Deribit-style option pricing (30s cache)      | No     |
| `getStrikeOptions(underlying, settings?)`    | Get filtered strikes grouped by expiry              | No     |
| `calculateLoan(params)`                      | Calculate exact costs (synchronous, BigInt math)    | No     |
| `isPromoOption(strike, price, expiry)`       | Check promotional pricing eligibility               | No     |
| `encodeRequestLoan(params)`                  | Encode tx for viem/wagmi                            | No     |
| `encodeAcceptOffer(...)`                     | Encode tx for viem/wagmi                            | No     |
| `encodeCancelLoan(id)`                       | Encode tx for viem/wagmi                            | No     |

***

## Loan Cost Formula

All costs are computed in USDC (6 decimals) using BigInt arithmetic:

```
OWE = depositAmount * strike / 10^(collateralDecimals + 8 - 6)
optionCost = askPrice * underlyingPrice * depositAmount / 10^collateralDecimals
capitalCost = OWE * (APR / 100) * durationInYears    (min 0.01 USDC)
protocolFee = OWE * 4 / 10000                         (4 basis points)
finalAmount = OWE - optionCost - capitalCost - protocolFee
```

### Promotional Pricing

Loans qualifying for promo get reduced costs:

* **Eligibility:** >90 days to expiry AND <50% LTV (strike/underlyingPrice)
* **Benefits:** Option premium waived, borrowing rate fixed at 5.68% APR
* **Limits:** $250,000 per person, $2,000,000 total pool

***

## Contract Addresses (Base)

| Contract               | Address                                      |
| ---------------------- | -------------------------------------------- |
| LoanCoordinator        | `0x9FB75b24d9d6f7c29D6BdE2870697A4FE0395994` |
| LoanHandler            | `0x7c444A2375275DaB925b32493B64a407eE955DEd` |
| USDC (settlement)      | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
| WETH (ETH collateral)  | `0x4200000000000000000000000000000000000006` |
| cbBTC (BTC collateral) | `0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf` |

***

## See Also

* [Borrowing Guide](/sdk/loan/borrowing) — Full borrowing workflow with code examples
* [Lending Guide](/sdk/loan/lending) — Fill limit orders and earn yield
* [Pricing & Calculation](/sdk/loan/pricing) — Strike selection and cost calculation details
* [Collar Loans](/sdk/loan/collar) — Zero-interest, capped-upside variant (`client.collar`)
* [RFQ Key Management](/sdk/rfq-factory/key-management) — ECDH keys used in loan auctions


# Borrowing

Request a loan by depositing ETH or BTC collateral. The SDK handles WETH wrapping, collateral approval, ECDH key generation, and event parsing automatically.

## Full Borrowing Flow

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

// Step 1: Browse available strikes
const groups = await client.loan.getStrikeOptions('ETH', {
  minDurationDays: 30,
  maxStrikes: 10,
  sortOrder: 'highestStrike',
  maxApr: 20,
});

for (const group of groups) {
  console.log(`\n${group.expiryFormatted}`);
  for (const opt of group.options) {
    const tag = opt.isPromo ? ' [PROMO]' : '';
    console.log(`  ${opt.strikeFormatted} | APR: ${opt.effectiveApr}%${tag}`);
  }
}

// Step 2: Calculate costs for a specific strike
const selected = groups[0].options[0];
const calc = client.loan.calculateLoan({
  depositAmount: '1.0',
  underlying: 'ETH',
  strike: selected.strike,
  expiryTimestamp: selected.expiry,
  askPrice: selected.askPrice,
  underlyingPrice: selected.underlyingPrice,
});

if (!calc) throw new Error('Invalid loan parameters');

console.log(`Receive:      ${calc.formatted.receive} USDC`);
console.log(`Repay:        ${calc.formatted.repay} USDC`);
console.log(`Option cost:  ${calc.formatted.optionCost} USDC`);
console.log(`Borrow fee:   ${calc.formatted.capitalCost} USDC`);
console.log(`Protocol fee: ${calc.formatted.protocolFee} USDC`);
console.log(`APR:          ${calc.formatted.apr}%`);
console.log(`Promo:        ${calc.isPromo}`);

// Step 3: Submit the loan request
const result = await client.loan.requestLoan({
  underlying: 'ETH',
  collateralAmount: '1.0',
  strike: selected.strike,
  expiryTimestamp: selected.expiry,
  minSettlementAmount: calc.finalLoanAmount,
  // keepOrderOpen is deprecated as of v0.2.1 (Base_r12) and ignored at the
  // contract level — the contract no longer supports converting an unfilled
  // RFQ into a limit order. The field remains in LoanRequest for source
  // compatibility but its value has no on-chain effect.
});

console.log(`TX: ${result.receipt.hash}`);
console.log(`Quotation ID: ${result.quotationId}`);
console.log(`Public Key: ${result.keyPair.compressedPublicKey}`);
```

***

## Auto-Wrapping ETH

When borrowing with ETH, `requestLoan()` automatically checks your WETH balance. If it's insufficient, it wraps the difference from native ETH before submitting the loan request.

```typescript
// If you have 0.5 WETH and request 1.0 ETH collateral,
// the SDK wraps 0.5 ETH → WETH automatically
const result = await client.loan.requestLoan({
  underlying: 'ETH',
  collateralAmount: '1.0',
  // ... other params
});
```

No auto-wrapping happens for BTC (cbBTC). You must hold sufficient cbBTC.

***

## Accept an Offer

When a market maker sends an encrypted offer during the RFQ auction, decrypt it and accept:

```typescript
// Decrypt the offer using the keypair from requestLoan()
const decrypted = await client.rfqKeys.decryptOffer(
  encryptedOfferData,
  offerorSigningKey
);

// Accept the offer
await client.loan.acceptOffer(
  quotationId,
  decrypted.offerAmount,
  BigInt(decrypted.nonce),
  offerorAddress
);
```

***

## Cancel a Loan Request

Cancel before any offer is accepted to reclaim your collateral:

```typescript
await client.loan.cancelLoan(quotationId);
```

***

## Query Loan State

### On-Chain State

```typescript
const state = await client.loan.getLoanRequest(quotationId);
console.log(state.requester);           // borrower address
console.log(state.collateralAmount);    // bigint
console.log(state.isSettled);           // true if an MM filled
console.log(state.settledOptionContract); // option address (if settled)
```

### From Indexer

```typescript
const userAddress = await signer.getAddress();
const loans = await client.loan.getUserLoans(userAddress);

for (const loan of loans) {
  console.log(`#${loan.quotationId} — ${loan.status}`);
  if (loan.optionAddress) {
    console.log(`  Option: ${loan.optionAddress}`);
  }
}
```

***

## At Expiry

Once the option reaches expiry, you have a 1-hour exercise window. Three choices:

### Exercise (Repay and Reclaim)

Repay the owed USDC amount and get your collateral back:

```typescript
// Ensure USDC is approved for the option contract
const optionInfo = await client.loan.getOptionInfo(optionAddress);
await client.erc20.ensureAllowance(
  '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC
  optionAddress,
  optionInfo.deliveryAmount
);

await client.loan.exerciseOption(optionAddress);
```

### Walk Away (Keep USDC)

If the collateral has dropped below the strike price, it may be better to keep the borrowed USDC:

```typescript
await client.loan.doNotExercise(optionAddress);
```

### Swap and Exercise

Swap collateral to USDC via a DEX aggregator, then exercise in one transaction. Useful when you want to repay without holding USDC:

```typescript
// Get swap data from a DEX aggregator (e.g., KyberSwap, 1inch)
const aggregatorAddress = '0x...';
const swapCalldata = '0x...';

await client.loan.swapAndExercise(
  optionAddress,
  aggregatorAddress,
  swapCalldata
);
```

***

## Encode for External Wallets

Generate transaction calldata for use with viem, wagmi, or any wallet library:

```typescript
const encoded = client.loan.encodeRequestLoan({
  underlying: 'ETH',
  collateralAmount: '1.0',
  strike: 1600,
  expiryTimestamp: 1780041600,
  minSettlementAmount: 1422410000n,
});

// Use with viem
await walletClient.sendTransaction({
  to: encoded.to as `0x${string}`,
  data: encoded.data as `0x${string}`,
});

// Also available:
client.loan.encodeAcceptOffer(quotationId, offerAmount, nonce, offeror);
client.loan.encodeCancelLoan(quotationId);
```

***

## See Also

* [Overview](/sdk/loan/overview) — Module overview and cost formula
* [Lending](/sdk/loan/lending) — Fill borrower limit orders
* [Pricing & Calculation](/sdk/loan/pricing) — Strike selection details


# Lending

Earn yield by filling borrowers' limit orders with USDC. You provide USDC now and receive the borrower's collateral (WETH or cbBTC) at expiry if the option is exercised.

## How Lending Works

> **Note (v0.2.1 / Base\_r12):** the contract no longer supports converting an unfilled RFQ into a limit order. The `keepOrderOpen` flag on `LoanRequest` is now a no-op deprecation. The lending workflow described below remains applicable only to historical loans tracked by the indexer that already carry that flag; new r12 loans will not surface here.

When a borrower enabled `keepOrderOpen: true` (pre-r12) and no market maker filled during the initial auction, the request became a limit order. Any user could fill it by providing the requested USDC amount.

At expiry, if the borrower exercises (repays), you receive your USDC back plus the spread. If the borrower walks away, you receive the collateral instead.

***

## Browse Lending Opportunities

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

// Fetch all available limit orders
const opps = await client.loan.getLendingOpportunities();

for (const o of opps) {
  console.log(`#${o.quotationId} | ${o.underlying}`);
  console.log(`  Collateral: ${o.collateralFormatted} ${o.underlying}`);
  console.log(`  Provide:    ${o.lendAmountFormatted} USDC`);
  console.log(`  Strike:     $${o.strike}`);
  console.log(`  Expiry:     ${o.expiryFormatted}`);
  console.log(`  APR:        ${o.aprFormatted}%`);
}
```

### Filter by Underlying

```typescript
// Only ETH-collateralized loans
const ethOpps = await client.loan.getLendingOpportunities({
  underlying: 'ETH',
});

// Only BTC-collateralized loans
const btcOpps = await client.loan.getLendingOpportunities({
  underlying: 'BTC',
});
```

### Exclude Your Own Loans

```typescript
const userAddress = await signer.getAddress();
const opps = await client.loan.getLendingOpportunities({
  excludeAddress: userAddress,
});
```

***

## Fill a Limit Order

Filling a limit order calls `OptionFactory.settleQuotation()` under the hood. You must approve USDC to the OptionFactory contract first.

```typescript
const opp = opps[0]; // Pick an opportunity

// Approve USDC to OptionFactory
const factoryAddress = client.chainConfig.contracts.optionFactory;
await client.erc20.ensureAllowance(
  '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC
  factoryAddress,
  opp.lendAmount
);

// Fill the loan
const receipt = await client.loan.lend(BigInt(opp.quotationId));
console.log(`Filled loan #${opp.quotationId}: ${receipt.hash}`);
```

***

## Lending Opportunity Fields

| Field                 | Type              | Description                        |
| --------------------- | ----------------- | ---------------------------------- |
| `quotationId`         | `string`          | Quotation ID to pass to `lend()`   |
| `requester`           | `string`          | Borrower's address                 |
| `underlying`          | `string`          | `'ETH'` or `'BTC'`                 |
| `collateralFormatted` | `string`          | Collateral amount (e.g. `"1.5"`)   |
| `lendAmountFormatted` | `string`          | USDC to provide (e.g. `"1422.41"`) |
| `lendAmount`          | `bigint`          | Raw USDC amount (6 decimals)       |
| `strike`              | `number`          | Strike price in USD                |
| `expiryTimestamp`     | `number`          | Expiry Unix timestamp              |
| `expiryFormatted`     | `string`          | Human-readable expiry date         |
| `apr`                 | `number`          | Estimated lender APR               |
| `aprFormatted`        | `string`          | APR as string (e.g. `"12.50"`)     |
| `raw`                 | `LoanIndexerLoan` | Full indexer response              |

***

## See Also

* [Overview](/sdk/loan/overview) — Module overview and cost formula
* [Borrowing](/sdk/loan/borrowing) — Request loans as a borrower


# Pricing & Calculation

Browse available strikes, calculate exact loan costs, and check promotional pricing eligibility.

## Fetch Strike Options

`getStrikeOptions()` fetches live pricing from the Deribit-style API, filters for valid OTM put options, calculates estimated APRs, and groups results by expiry date.

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider });

// ETH strikes with default settings
const groups = await client.loan.getStrikeOptions('ETH');

// BTC strikes with custom filters
const btcGroups = await client.loan.getStrikeOptions('BTC', {
  minDurationDays: 30,   // At least 30 days to expiry
  maxStrikes: 5,         // Top 5 strikes per expiry
  sortOrder: 'highestStrike',
  maxApr: 15,            // Borrowing rate for cost estimation
});
```

### Strike Settings

| Setting           | Default           | Description                                                                |
| ----------------- | ----------------- | -------------------------------------------------------------------------- |
| `minDurationDays` | `7`               | Minimum days until expiry                                                  |
| `maxStrikes`      | `20`              | Maximum strikes per expiry group                                           |
| `sortOrder`       | `'highestStrike'` | `'highestStrike'`, `'lowestStrike'`, `'nearestExpiry'`, `'furthestExpiry'` |
| `maxApr`          | `20`              | Borrowing APR used for cost estimation (not a filter)                      |

### Strike Option Fields

Each option in a group contains:

| Field               | Type      | Description                               |
| ------------------- | --------- | ----------------------------------------- |
| `strike`            | `number`  | Strike price in USD                       |
| `strikeFormatted`   | `string`  | `"$1,600"`                                |
| `expiry`            | `number`  | Unix timestamp                            |
| `expiryFormatted`   | `string`  | `"Fri, March 28, 2025"`                   |
| `expiryLabel`       | `string`  | Deribit format: `"28MAR25"`               |
| `underlyingPrice`   | `number`  | Current underlying price                  |
| `askPrice`          | `number`  | Option ask price (fraction of underlying) |
| `impliedLoanAmount` | `number`  | Estimated receive per unit of collateral  |
| `effectiveApr`      | `number`  | All-in APR including all costs            |
| `isPromo`           | `boolean` | Promotional pricing eligible              |

***

## Calculate Loan Costs

`calculateLoan()` is synchronous — pure BigInt math, no network calls. Use it after selecting a strike from `getStrikeOptions()`.

```typescript
const calc = client.loan.calculateLoan({
  depositAmount: '2.5',       // 2.5 ETH
  underlying: 'ETH',
  strike: 1800,               // $1,800 strike
  expiryTimestamp: 1780041600, // Unix seconds
  askPrice: 0.007,             // from strike option
  underlyingPrice: 2500,       // from strike option
  maxApr: 20,                  // borrowing rate (default: 20)
});

if (calc) {
  // Raw BigInt values (USDC, 6 decimals)
  console.log(calc.owe);             // Total owed at expiry
  console.log(calc.optionCost);      // Option premium
  console.log(calc.capitalCost);     // Borrowing fee
  console.log(calc.protocolFee);     // Protocol fee (4 bps)
  console.log(calc.totalCosts);      // Sum of all costs
  console.log(calc.finalLoanAmount); // What you receive

  // Formatted strings
  console.log(calc.formatted.receive);    // "4198.23"
  console.log(calc.formatted.repay);      // "4500.00"
  console.log(calc.formatted.optionCost); // "43.7500"
  console.log(calc.formatted.capitalCost);// "98.6301"
  console.log(calc.formatted.protocolFee);// "1.8000"
  console.log(calc.formatted.apr);        // "14.52"

  // Use finalLoanAmount as minSettlementAmount in requestLoan()
  await client.loan.requestLoan({
    underlying: 'ETH',
    collateralAmount: '2.5',
    strike: 1800,
    expiryTimestamp: 1780041600,
    minSettlementAmount: calc.finalLoanAmount,
  });
}
```

Returns `null` if inputs are invalid (zero deposit, zero strike, etc.) or if the final loan amount would be negative.

***

## Promotional Pricing

Check if a specific strike qualifies for promotional rates:

```typescript
const isPromo = client.loan.isPromoOption(
  1200,           // strike price in USD
  2500,           // current underlying price
  1790041600,     // expiry timestamp
  50000,          // estimated loan amount in USD (optional, for $250k cap)
);

console.log(isPromo); // true if eligible
```

### Promo Eligibility Rules

| Condition      | Requirement                         |
| -------------- | ----------------------------------- |
| Days to expiry | > 90 days                           |
| LTV ratio      | < 50% (strike / underlyingPrice)    |
| Loan amount    | < $250,000 per person               |
| Total pool     | < $2,000,000 across all promo loans |

When promo applies:

* Option premium is waived (`optionCost = 0`)
* Borrowing APR is fixed at 5.68%

***

## Raw Pricing Data

Access the underlying Deribit-style pricing API directly:

```typescript
const pricing = await client.loan.fetchPricing();

// pricing['ETH']['ETH-28MAR25-3000-P'] = {
//   underlying_price: 2500,
//   ask_price: 0.007,
//   mark_price: 0.0065,
// }

// Cached for 30 seconds — subsequent calls return cached data
const cached = await client.loan.fetchPricing(); // instant if < 30s
```

***

## See Also

* [Overview](/sdk/loan/overview) — Module overview and cost formula
* [Borrowing](/sdk/loan/borrowing) — Full borrowing workflow
* [Lending](/sdk/loan/lending) — Fill limit orders and earn yield


# Collar Loans

Collar loans are a **zero-interest, capped-upside** variant of the standard physically-settled call loan. The borrower buys a put at `K_lo` (default trigger) and sells a call at `K_hi` (cap); the lender funds an up-front USDC loan from the call premium it earns.

> **Status:** the `CollarLoanCoordinator` contract is not yet deployed on Base mainnet (placeholder addresses in `src/chains/collar.ts`). Pricing math (`estimateCollar`, `getCapStrikeOptions`, `filterCapStrikes`) works today against live Deribit data; write methods (`requestLoan`, `cancelLoan`, `acceptOffer`) throw `NETWORK_UNSUPPORTED` until the addresses are populated.

## Terminal payoff

At expiry (TWAP `S`):

| Region            | Borrower outcome                                           |
| ----------------- | ---------------------------------------------------------- |
| `S < K_lo`        | Walk: keep `L` USDC, lose collateral `N`                   |
| `K_lo ≤ S ≤ K_hi` | Repay `L` USDC, recover `N`                                |
| `S > K_hi`        | Cap settlement: receive `N · (K_hi − K_lo)` USDC, lose `N` |

## Pricing model (frontend estimate)

```
target_put_premium = call_premium × (1 − mm_margin)
K_lo               = highest OTM put strike whose ask ≤ target_put_premium
L                  = K_lo · N
cap_payout         = (K_hi − K_lo) · N
```

`mm_margin` defaults to 4% and is tunable per call. When the call premium is too small to fund any OTM put, the estimator falls back to the cheapest available put strike so the row still surfaces.

## Shared infrastructure (do not re-implement)

`CollarModule` deliberately reuses the loan module's plumbing instead of duplicating it:

* **`client.collar.fetchPricing()` delegates to `client.loan.fetchPricing()`.** The 30-second pricing cache is shared between both modules — calling either one populates the same cached map. Don't add a second cache.
* **Expiry parsing lives in `utils/expiry.ts`.** Use `parseDeribitExpiry(label)` (returns `number | null`) or `parseDeribitExpiryOrThrow(label)`. Same for `formatDeribitExpiry`. Both `LoanModule` and `CollarModule` import from there.
* **`DeribitPricingMap` and `DeribitOptionData` types live in `types/loan.ts`.** The collar leg uses the optional `bid_price` field; the put leg ignores it.

## Examples

```ts
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider, signer });

// 1. Read pricing math without any on-chain calls
const pricingData = await client.collar.fetchPricing();   // uses shared cache
const spot = client.collar.extractUnderlyingPrice(pricingData, 'BTC');

const est = client.collar.estimateCollar({
  underlying: 'BTC',
  collateralAmount: 0.5,
  capUsd: 150_000,
  expiryLabel: '26DEC25',
  pricingData,
  underlyingPrice: spot,
});
// est = { loanUsd, triggerUsd, capPayoutUsd, callBtc, putBtc, putStrike } | null

// 2. Build a UI strike picker
const groups = await client.collar.getCapStrikeOptions('BTC', {
  ...client.collar.defaultSettings,
  collateralAmount: 0.5,
});
// groups[].caps[].estimate carries the per-row loanUsd, triggerUsd, capPayoutUsd

// 3. Post an auction (requires deployed coordinator + signer)
if (client.collar.isDeployed()) {
  const { quotationId, txHash } = await client.collar.requestLoan({
    underlying: 'BTC',
    collateralAmount: '0.5',
    capUsd: 150_000,
    minLoanUsd: 65_000,
    expiryTimestamp: 1798934400,
    requesterPublicKey: await client.rfqKeys.getOrCreateKeyPair().then(k => k.publicKey),
  });
}
```

## Default settings (asset-aware)

```ts
client.collar.defaultSettings
// {
//   minDurationDays: 30,
//   minCapStrikeUsd: 0,        // 0 disables — set per asset, see note
//   minCapGapPct: 20,
//   maxStrikesPerExpiry: 6,
//   mmMarginPct: 4,
//   reserveFloorPct: 90,
// }
```

`minCapStrikeUsd` defaults to `0` (disabled) because a single dollar floor can't fit both BTC (spot ~~$90k) and ETH (~~$2k). Set a per-asset floor at call time if your UI needs one (e.g. BTC=$100k, ETH=$3k).

## Smoke tests

```bash
# Offline (synthetic Deribit fixture)
npx tsx scripts/test-collar-module.ts

# + live Deribit fetch (verifies BTC and ETH paths against real data)
npx tsx scripts/test-collar-module.ts --live
```

26 assertions cover capability gating, math correctness, null guards, dedup behavior, the asset-aware default, and the shared cache identity between `collar.fetchPricing` and `loan.fetchPricing`.

## Reference

The contract layout mirrors the existing `LoanModule` — a coordinator wraps Thetanuts V4 RFQ auctions and instantiates a `CollaredCallOption` proxy on settle. See `src/abis/collar.ts` for the coordinator ABI and `CollaredCallOption` exercise surface.

> Note: the ABIs in `src/abis/collar.ts` are a best-effort port of the `collar.html` reference at `https://thetanuts.finance/dev/zendfi_v1/collar.html` (which itself uses zero placeholder addresses) plus extrapolation from `LOAN_OPTION_ABI` for the option exercise surface. They are not verified against a deployed contract yet — replace with the live contract ABIs when collar-v12 ships.


# Overview

Deposit into wheel-strategy vaults on Ethereum mainnet. Each vault sells covered calls and cash-secured puts on a single underlying (WBTC, XAUt, SPYon) and routes premiums back to LPs.

## Chain

`client.wheelVault` is **Ethereum mainnet only** (`chainId 1`). Calls throw `NETWORK_UNSUPPORTED` when the client is configured for any other chain. To use this module, instantiate the client with `chainId: 1`.

```typescript
const client = new ThetanutsClient({ chainId: 1, provider, signer });
```

***

## How It Works

A WheelVault holds a paired position (e.g. WBTC + USDC) and sells weekly options against it. Each vault has multiple "series" — each series is one cohort of LPs sharing one option-selling cycle.

1. **LPs deposit** the vault's two assets and receive shares pinned to a series
2. **Vault sells options** when triggered (calls when shares are quote-heavy, puts when base-heavy)
3. **Buyers fill** through the Markets contract, paying premium per contract
4. **At expiry**, options either expire worthless (LPs keep premium) or get exercised (LPs deliver underlying at the strike)
5. **LPs withdraw** their share of the post-cycle vault state

The Markets and Lens layers expose the order book, depth chart, and per-buyer/per-seller views.

***

## Quick Start

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient, WHEEL_VAULT_CONFIG } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://eth.llamarpc.com');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 1, provider, signer });

const wbtcVault = WHEEL_VAULT_CONFIG.assets.WBTC.vault;

// Read state of the active series
const seriesId = (await client.wheelVault.getSeriesCount(wbtcVault)) - 1;
const state = await client.wheelVault.getVaultState(wbtcVault, seriesId);
console.log(state.baseBalance, state.quoteBalance, state.totalShares);

// Preview a deposit (returns expected shares minted)
const expectedShares = await client.wheelVault.previewDeposit(
  wbtcVault,
  seriesId,
  ethers.parseUnits('0.01', 8),  // 0.01 WBTC
  ethers.parseUnits('1000', 6),  // 1000 USDC
);

// Deposit (requires prior ERC20 approvals on both base and quote tokens)
const result = await client.wheelVault.deposit(
  wbtcVault,
  seriesId,
  ethers.parseUnits('0.01', 8),
  ethers.parseUnits('1000', 6),
  state.lastPrice,  // expectedPrice for slippage check
);
console.log(`Shares minted: ${result.sharesMinted}`);
```

***

## Method Overview

### Vault state (read-only)

| Method                                                 | Description                                                            |
| ------------------------------------------------------ | ---------------------------------------------------------------------- |
| `getVaultState(vault, seriesId)`                       | Full state snapshot: balances, shares, last price, options outstanding |
| `getSeries(vault, seriesId)`                           | Raw on-chain series struct                                             |
| `getSeriesCount(vault)`                                | Number of series in the vault (use `count - 1` for the active one)     |
| `getSnapshots(vault, seriesId)`                        | Historical share-value snapshots for accounting                        |
| `getEpochExpiries(vault, seriesId)`                    | Per-epoch option expiry timestamps                                     |
| `getShareValueInQuote(vault, seriesId, shares)`        | Mark a share balance to quote-asset value                              |
| `getSeriesAssets(vault, seriesId)`                     | Total base + quote held by the series                                  |
| `bsBaseDelta(vault, seriesId, price?)`                 | Black-Scholes base delta for the active option                         |
| `previewDeposit(vault, seriesId, baseAmt, quoteAmt)`   | Pre-flight: expected shares minted                                     |
| `previewWithdraw(vault, seriesId, shares)`             | Pre-flight: expected base/quote returned                               |
| `estimateDepositSplit(vault, seriesId, depositAmount)` | Suggest how to split a single-asset deposit                            |

### Vault writes

| Method                                                       | Description                                                    | Signer |
| ------------------------------------------------------------ | -------------------------------------------------------------- | ------ |
| `deposit(vault, seriesId, baseAmt, quoteAmt, expectedPrice)` | Deposit both assets, receive shares                            | Yes    |
| `withdraw(vault, seriesId, shares)`                          | Burn shares, receive pro-rata assets                           | Yes    |
| `withdrawIdle(vault, seriesId)`                              | Withdraw shares not deployed in current option                 | Yes    |
| `depositSingle(params)`                                      | Router-side single-asset deposit (auto-splits via swap)        | Yes    |
| `depositDual(params)`                                        | Router-side dual-asset deposit                                 | Yes    |
| `withdrawSingle(params)`                                     | Router-side single-asset withdraw                              | Yes    |
| `withdrawSingleWithPermit(params)`                           | Same as `withdrawSingle` with EIP-2612 permit                  | Yes    |
| `poke(vault)`                                                | Settle expired options without triggering a new one            | Yes    |
| `trigger(vault)`                                             | Settle expired options AND start the next option-selling cycle | Yes    |

### Markets (option order book)

| Method                             | Description                                    | Signer |
| ---------------------------------- | ---------------------------------------------- | ------ |
| `marketFill(markets, params)`      | Buyer-side: fill outstanding sell orders at IV | Yes    |
| `depositToBucket(markets, params)` | Place a buy order at a target IV bucket        | Yes    |
| `cancelDeposit(markets, entryId)`  | Cancel an outstanding bucket deposit           | Yes    |
| `claim(markets, token)`            | Claim accrued premium/payout in a token        | Yes    |
| `exercise(markets, optionId)`      | Exercise an in-the-money option held as buyer  | Yes    |
| `expire(markets, optionId)`        | Settle an expired option                       | Yes    |
| `swapAndExercise(markets, params)` | Swap → exercise in one transaction             | Yes    |

### Lens helpers (read-only views)

| Method                                                       | Description                                     |
| ------------------------------------------------------------ | ----------------------------------------------- |
| `getDepthChart(lens, underlying, optionExpiry)`              | Depth-chart data across IV buckets              |
| `previewFillPremium(lens, premiumPerContract, trancheIndex)` | Preview premium for a partial fill              |
| `getBuyerOptions(lens, buyer)`                               | All options held by a buyer address             |
| `getSellerPositions(lens, seller)`                           | All seller exposures across markets             |
| `getClaimableSummary(lens, address)`                         | Aggregate claimable amounts per token           |
| `previewExercise(lens, optionId)`                            | Pre-flight: settlement amounts for an exercise  |
| `getUniswapPositions(positionIds)`                           | Decode V3 NFT positions referenced by the vault |

`multicall(calls)` is also exposed for batching low-level reads.

***

## Configuration

The `WHEEL_VAULT_CONFIG` constant exports vault, markets, lens, router, and shared infrastructure addresses. Use it to look up addresses without hardcoding them.

```typescript
import { WHEEL_VAULT_CONFIG } from '@thetanuts-finance/thetanuts-client';

WHEEL_VAULT_CONFIG.assets.WBTC.vault;        // 0x77D5d8c8...
WHEEL_VAULT_CONFIG.assets.WBTC.markets;      // 0x38E7ab2D...
WHEEL_VAULT_CONFIG.assets.WBTC.marketsLens;  // 0x5C28f508...
WHEEL_VAULT_CONFIG.contracts.router;         // 0x53A14b15...
WHEEL_VAULT_CONFIG.contracts.lens;           // 0xf5b1D7B8...
```

***

## Contract Addresses (Ethereum)

### Vaults

| Asset | Vault                                        | Markets                                      | Markets Lens                                 |
| ----- | -------------------------------------------- | -------------------------------------------- | -------------------------------------------- |
| WBTC  | `0x77D5d8c86cC66f95Fa8cdacFa7105dF0BC4d9AA9` | `0x38E7ab2D8b6c3e6149B8085f34E8C832c5eFDAD1` | `0x5C28f508529c097dc26c86f6b74D0249ae17eb6e` |
| XAUt  | `0x6C8753ACCbB9d370D63d684c03D607C91b8E1602` | `0x249a7f751382Fc39A50bd5Dc0CC17f4d54af78Bc` | `0xbD42297982BA6A65aDA1D9B759fcF855395157C0` |
| SPYon | `0xAe3bae89890213c43FeEe4B98b7e34645652cFe1` | `0xe6656Fc4360023Af9a6d8917a74563B9ce2fe91F` | `0x0709b125936FAf0022a23CB295Cb298faf4d137a` |

### Shared infrastructure

| Contract       | Address                                      |
| -------------- | -------------------------------------------- |
| Router         | `0x53A14b15CaDBB02725B8ABf781a5e91bdB1bC1Ab` |
| Lens           | `0xf5b1D7B8885B40676A9f27e979F8F6d8e7D4fcD1` |
| USDC (quote)   | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` |
| Uniswap V3 NPM | `0xC36442b4a4522E871399CD717aBDD847Ab11FE88` |
| Multicall3     | `0xcA11bde05977b3631167028862bE2a173976CA11` |

Markets constants: `ivMin = 2000` (20%), `ivMax = 20000` (200%), `ivTick = 500` (5%), `exerciseWindow = 3600s`.

***

## See Also

* [Modules Overview](/sdk/sdk-reference/modules-overview) — All client modules at a glance
* [Strategy Vault](/sdk/strategy-vault-base/overview) — Base-side fixed-strike + CLVEX vaults
* [Token Operations](/sdk/guides/token-operations) — ERC20 approvals required before deposits


# Overview

Deposit into fixed-strike call vaults and CLVEX directional/condor strategy vaults on Base. Each vault rolls weekly options against deposited collateral and pays accrued yield to share-holders.

## Chain

`client.strategyVault` is **Base only** (`chainId 8453`). Calls throw `NETWORK_UNSUPPORTED` when the client is configured for any other chain.

```typescript
const client = new ThetanutsClient({ chainId: 8453, provider, signer });
```

***

## How It Works

There are two vault families behind one module:

* **Fixed-strike vaults** sell covered calls on aBasWETH (Aave-wrapped WETH on Base) at a strike encoded in the vault name (`ETH-3000`, `ETH-3500`, etc.). Each Friday, the vault writes a weekly call at its strike against deposited collateral; LPs collect premium.
* **CLVEX vaults** run pre-defined directional or condor strategies (`bull`, `bear`, `condor`). The vault decides which structure to write each week based on its strategy.

A vault has one share-class (no series), so depositors mix into a single pool and share gains/losses pro-rata.

1. **LPs deposit** the vault's base asset (e.g. aBasWETH) and receive shares
2. **Vault calls `createOption()`** when eligible (typically once per cycle, gated by `canCreateOption()`)
3. **At expiry**, the option is settled against the underlying price; LPs absorb gains or losses
4. **LPs withdraw** by burning shares for pro-rata vault assets

The module exposes shortcuts to enumerate the live vault rosters (`getFixedStrikeVaults`, `getClvexVaults`).

***

## Quick Start

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient, STRATEGY_VAULT_CONFIG } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

// 1. List all live vaults (both families)
const states = await client.strategyVault.getAllVaults();
states.forEach((s) => console.log(s.address, s.totalAssets));

// 2. Pick a fixed-strike vault (e.g. ETH-3000)
const vaultAddress = STRATEGY_VAULT_CONFIG.fixedStrike.vaults[0].address;
const state = await client.strategyVault.getVaultState(vaultAddress);

// 3. Deposit (assetIndex 0 = base asset, e.g. aBasWETH for fixed-strike)
const result = await client.strategyVault.deposit(
  vaultAddress,
  ethers.parseUnits('1.0', 18),
  0,
);
console.log(`Deposit tx: ${result.receipt.hash}`);

// 4. Later: check share balance, then withdraw
const myAddress = await signer.getAddress();
const shares = await client.strategyVault.getShareBalance(vaultAddress, myAddress);
await client.strategyVault.withdraw(vaultAddress, shares);
```

***

## Method Overview

### Discovery

| Method                           | Description                                    |
| -------------------------------- | ---------------------------------------------- |
| `getAllVaults()`                 | Live state of every fixed-strike + CLVEX vault |
| `getFixedStrikeVaults()`         | Live state of fixed-strike vaults only         |
| `getClvexVaults()`               | Live state of CLVEX strategy vaults only       |
| `getAllVaultStates(addresses[])` | Batch state lookup for an arbitrary vault list |

### State (read-only)

| Method                            | Description                                                |
| --------------------------------- | ---------------------------------------------------------- |
| `getVaultState(vault)`            | Full snapshot: assets, shares, next expiry, recovery state |
| `getTotalAssets(vault)`           | Base + quote assets currently held                         |
| `getShareBalance(vault, address)` | Share balance for a user                                   |
| `getNextExpiry(vault)`            | Unix timestamp for the next option expiry                  |
| `canCreateOption(vault)`          | Whether `createOption()` is eligible right now             |
| `isRecoveryMode(vault)`           | Whether the vault is paused for emergency withdrawals      |

### Writes

| Method                               | Description                                                           | Signer |
| ------------------------------------ | --------------------------------------------------------------------- | ------ |
| `deposit(vault, amount, assetIndex)` | Deposit base or quote asset, receive shares                           | Yes    |
| `withdraw(vault, shares)`            | Burn shares, receive pro-rata assets                                  | Yes    |
| `createOption(vault)`                | Trigger the next option-selling cycle (anyone can call when eligible) | Yes    |

`assetIndex` selects which side of the vault you're depositing — `0` = base asset, `1` = quote asset (vaults that accept both). For fixed-strike, base = aBasWETH and quote = aBasUSDC.

***

## Configuration

```typescript
import { STRATEGY_VAULT_CONFIG } from '@thetanuts-finance/thetanuts-client';

STRATEGY_VAULT_CONFIG.fixedStrike.vaults;       // 5 fixed-strike ETH vaults
STRATEGY_VAULT_CONFIG.clvex.vaults;             // 3 strategy vaults (bull/bear/condor)
STRATEGY_VAULT_CONFIG.fixedStrike.baseAsset;    // aBasWETH
STRATEGY_VAULT_CONFIG.fixedStrike.quoteAsset;   // aBasUSDC
STRATEGY_VAULT_CONFIG.optionFactory;       // OptionFactory used by vaults
```

***

## Contract Addresses (Base)

### Fixed-strike (ETH calls)

| Name     | Address                                      | Strike |
| -------- | -------------------------------------------- | ------ |
| ETH-3000 | `0x5189180C5Bb1bB54f8479a6aeFdFFEd66Ea0951b` | 3000   |
| ETH-3500 | `0xf70088De12E325562dEbfd7740089d894d5b23ce` | 3500   |
| ETH-4000 | `0xf4BeE19920B7672A763e40FAD720714B7B1cb7aa` | 4000   |
| ETH-4500 | `0x05701eE7269b5Cd36660e9A62C9Fc6B7B67FfF12` | 4500   |
| ETH-5000 | `0xE0f808f7717157627139dA38F1226E7011582b67` | 5000   |

Fixed-strike base asset: aBasWETH `0xD4a0e0b9149BCee3C920d2E00b5dE09138fd8bb7` Fixed-strike quote asset: aBasUSDC `0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB`

### CLVEX (strategy vaults)

| Name         | Address                                      | Strategy |
| ------------ | -------------------------------------------- | -------- |
| CLVEX Bull   | `0xeD4c7897D5f1BD8cD00297B3348Fe558D2ABF2Ff` | bull     |
| CLVEX Bear   | `0x07E7a12D9CFc5bc18f578D7C400B26741fc699BE` | bear     |
| CLVEX Condor | `0xFB073625088014fe4826ae4Ab7Cde12B922Ba5F2` | condor   |

### Shared

| Contract                 | Address                                      | Notes                                                                                                                                                                                                 |
| ------------------------ | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Vault-side OptionFactory | `0x1D1Fee494dDEAF32626dcd50e0Cd83890574730f` | Used internally by the strategy vaults to mint weekly options. Distinct from `chainConfig.contracts.optionFactory` (`0x8118daD9…`), which is the canonical user-facing OptionFactory for direct RFQs. |

***

## See Also

* [Modules Overview](/sdk/sdk-reference/modules-overview) — All client modules at a glance
* [Wheel Vault](/sdk/wheel-vault-ethereum/overview) — Ethereum-side wheel-strategy vaults
* [Token Operations](/sdk/guides/token-operations) — ERC20 approvals required before deposits


# MM Pricing Overview

Fetch indicative market maker prices with fee adjustments and collateral carrying costs for vanilla options and multi-leg structures.

## Overview

The `MMPricingModule` fetches prices from market makers and applies two adjustments:

1. **Fee Adjustment** - Widens the bid-ask spread to account for exchange fees
2. **Collateral Carrying Cost** - Adds the opportunity cost of capital for short positions

### Importing

```typescript
import {
  MMPricingModule,
  applyFeeAdjustment,
  calculateCollateralCost,
  parseTicker,
  buildTicker,
  COLLATERAL_APR,
} from '@thetanuts-finance/thetanuts-client';
```

### Module Initialization

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453 });
const mmPricing = client.mmPricing;
```

***

## Fee Adjustment Formula

Market maker prices are adjusted to account for exchange fees:

```
feeAdjustment = min(0.0003, rawPrice × 0.125)
```

* **Ask price:** `feeAdjustedAsk = rawAsk + feeAdjustment`
* **Bid price:** `feeAdjustedBid = rawBid - feeAdjustment`

The adjustment is capped at 0.0003 (0.03% of 1 contract) to prevent excessive spread widening for high-premium options.

```typescript
import { applyFeeAdjustment } from '@thetanuts-finance/thetanuts-client';

const rawBid = 0.02;
const rawAsk = 0.025;

const adjustedBid = applyFeeAdjustment(rawBid, 'bid');
const adjustedAsk = applyFeeAdjustment(rawAsk, 'ask');

// feeAdjustment = min(0.0003, 0.02 * 0.125) = 0.0003
// adjustedBid = 0.02 - 0.0003 = 0.0197
// adjustedAsk = 0.025 + 0.0003 = 0.0253
```

***

## Collateral Carrying Cost

Short positions require locked collateral with an opportunity cost. The SDK applies per-asset APR rates:

| Asset | APR |
| ----- | --- |
| BTC   | 1%  |
| ETH   | 4%  |
| USD   | 7%  |

```
collateralCost = collateralValue × APR × timeToExpiry
```

Where `timeToExpiry` is expressed in years.

```typescript
import { calculateCollateralCost, COLLATERAL_APR } from '@thetanuts-finance/thetanuts-client';

console.log(COLLATERAL_APR);
// { BTC: 0.01, ETH: 0.04, USD: 0.07 }

// PUT at strike 2000, 30 days to expiry
const cost = calculateCollateralCost(2000, 30 / 365, 'USD');
// cost = 2000 × 0.07 × (30/365) = $11.51
```

***

## getAllPricing

Fetches all option prices for an underlying asset, grouped by expiry.

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453 });

const allETH = await client.mmPricing.getAllPricing('ETH');
const allBTC = await client.mmPricing.getAllPricing('BTC');

// Returns: MMAllPricingResponse
// {
//   expirations: [
//     {
//       expiry: 1710547200,
//       expiryLabel: '16MAR24',
//       options: [
//         { strike: 1800, isCall: true, bid: 0.05, ask: 0.055, ... },
//         { strike: 1800, isCall: false, bid: 0.02, ask: 0.025, ... },
//       ]
//     }
//   ]
// }
```

***

## getTickerPricing

Fetches fee-adjusted pricing for a specific option ticker. Returns per-collateral breakdowns.

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453 });

const pricing = await client.mmPricing.getTickerPricing('ETH-16FEB26-1800-P');

console.log(pricing.rawBidPrice);
console.log(pricing.rawAskPrice);
console.log(pricing.feeAdjustedBid);
console.log(pricing.feeAdjustedAsk);

// Per-collateral breakdowns (include collateral carrying cost)
console.log(pricing.byCollateral.USD.mmBidPrice);
console.log(pricing.byCollateral.USD.mmAskPrice);
console.log(pricing.byCollateral.ETH.mmBidPrice);
console.log(pricing.byCollateral.ETH.mmAskPrice);
```

***

## getPricingArray

Convenience wrapper that flattens `getAllPricing(...)` into a single sorted, non-expired array. Useful when you want to paginate or filter across all expiries at once instead of walking the nested `expirations[]` shape.

```typescript
const all = await client.mmPricing.getPricingArray('ETH');

// Already sorted by expiry, then strike. Expired options filtered out.
const calls = all.filter((p) => p.isCall);
const nearTerm = all.filter((p) => p.expiry < someThreshold);
```

For richer slicing (by date range, strike range, type), pair this with the helpers in [Filters & Utilities](/sdk/pricing/filters-utilities).

***

## MMPricingModule Method Table

| Method                                     | Description                             | Signer Required |
| ------------------------------------------ | --------------------------------------- | --------------- |
| `getAllPricing(underlying)`                | All pricing for ETH/BTC                 | No              |
| `getTickerPricing(ticker)`                 | Pricing for specific ticker             | No              |
| `getPositionPricing(params)`               | Long/short pricing with collateral cost | No              |
| `getSpreadPricing(params)`                 | 2-leg spread pricing                    | No              |
| `getButterflyPricing(params)`              | 3-leg butterfly pricing                 | No              |
| `getCondorPricing(params)`                 | 4-leg condor pricing                    | No              |
| `filterExpired(pricing[])`                 | Filter out expired options              | No              |
| `sortByExpiryAndStrike(pricing[])`         | Sort by expiry, then strike             | No              |
| `getUniqueExpiries(pricing[])`             | Get unique expiry dates                 | No              |
| `filterByType(pricing[], isCall)`          | Filter calls or puts                    | No              |
| `filterByExpiry(pricing[], date)`          | Filter by expiry date                   | No              |
| `filterByStrikeRange(pricing[], min, max)` | Filter by strike range                  | No              |
| `getPricingArray(underlying)`              | Sorted, non-expired pricing array       | No              |

***

## Price Components Summary

| Component       | Description                         | Applies To |
| --------------- | ----------------------------------- | ---------- |
| Raw Price       | Market maker's base price           | All        |
| Fee Adjustment  | Exchange fee spread widening        | All        |
| Collateral Cost | Cost of capital for short positions | Short only |

**Final Ask (buying/going long):**

```
finalAsk = rawAsk + feeAdjustment
```

**Final Bid (selling/going short):**

```
finalBid = rawBid - feeAdjustment - collateralCost
```

***

## See Also

* [Position & Spread Pricing](/sdk/pricing/position-spread-pricing) - `getPositionPricing` and `getSpreadPricing` with collateral cost details
* [Collateral Cost Reference](/sdk/pricing/collateral-cost) - APR rates and the carrying cost formula
* [Filters & Utilities](/sdk/pricing/filters-utilities) - Filter and sort helpers for pricing arrays


# Position & Spread Pricing

Fetch position-level pricing (with collateral cost) for single options and multi-leg spreads, and integrate `rfqCalculations`, `mmPricing`, and `optionFactory` into a complete RFQ workflow.

## getPositionPricing

Returns pricing for a specific option position including collateral cost. Use this when you know the direction (long/short), contract count, and collateral token.

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453 });

const position = await client.mmPricing.getPositionPricing({
  ticker: 'ETH-16FEB26-1800-P',
  isLong: true,         // true = BUY, false = SELL
  numContracts: 10,
  collateralToken: 'USDC',
});

console.log(position.basePremium);     // Fee-adjusted price before carrying cost
console.log(position.collateralCost);  // Carrying cost added for short positions
console.log(position.totalPrice);      // Final all-in price including collateral cost
```

For short positions, `collateralCost` is calculated as:

```
collateralCost = collateralValue × APR × timeToExpiry
```

where APR is 1% for BTC, 4% for ETH, 7% for USD collateral.

***

## getSpreadPricing

Returns net spread pricing for a two-leg structure. Multi-leg structures use a **spread-level** collateral cost based on the spread width (max loss), not the sum of per-leg costs.

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453 });

const spread = await client.mmPricing.getSpreadPricing({
  underlying: 'ETH',
  strikes: [220000000000n, 200000000000n],  // 8 decimals: $2200, $2000
  expiry: 1774627200,
  isCall: false,  // Put spread
});

// Width and collateral cost
console.log('Width (USD):', spread.widthUsd);                // e.g. 200
console.log('Spread CC (USD):', spread.spreadCollateralCost); // e.g. ~$0.42
console.log('Net spread price (ETH):', spread.netSpreadPrice);

// Net MM prices already include the spread-level collateral cost
const ethPrice = spread.nearLeg.underlyingPrice;
console.log('Ask (USD):', (spread.netMmAskPrice * ethPrice).toFixed(2));
console.log('Bid (USD):', (spread.netMmBidPrice * ethPrice).toFixed(2));
```

***

## getButterflyPricing

Returns net pricing for a three-leg butterfly. Strikes are passed in the order `[lower, middle, upper]` (8-decimal price units). Width-based collateral cost mirrors the spread case.

```typescript
const fly = await client.mmPricing.getButterflyPricing({
  underlying: 'ETH',
  strikes: [180000000000n, 200000000000n, 220000000000n],  // $1800, $2000, $2200
  expiry: 1774627200,
  isCall: true,
});

console.log('Width (USD):', fly.widthUsd);
console.log('Net ask (ETH):', fly.netMmAskPrice);
console.log('Net bid (ETH):', fly.netMmBidPrice);
```

***

## getCondorPricing

Returns net pricing for a four-leg condor (`[strike1, strike2, strike3, strike4]`, ascending). Same collateral-cost model as butterfly — based on the wider wing.

```typescript
const condor = await client.mmPricing.getCondorPricing({
  underlying: 'ETH',
  strikes: [
    160000000000n,  // $1600
    180000000000n,  // $1800
    220000000000n,  // $2200
    240000000000n,  // $2400
  ],
  expiry: 1774627200,
  isCall: false,
});

console.log('Net ask (ETH):', condor.netMmAskPrice);
console.log('Net bid (ETH):', condor.netMmBidPrice);
```

For iron condors and richer 4-strike combinations, validate parameters with `validateCondor` / `validateIronCondor` from the rfqCalculations helpers before submitting.

***

## How rfqCalculations, mmPricing, and optionFactory Fit Together

The three modules divide responsibilities cleanly:

| Module            | Purpose                                                         |
| ----------------- | --------------------------------------------------------------- |
| `rfqCalculations` | Position sizing: `numContracts`, `collateral`, `reservePrice`   |
| `mmPricing`       | Price discovery: fee-adjusted bid/ask, collateral carrying cost |
| `optionFactory`   | RFQ submission and settlement lifecycle                         |

### Workflow

```
User Input (amount, product, strikes, direction)
       │
       ▼
1. Get MM pricing        → mmPricing.getTickerPricing() / getSpreadPricing()
       │
       ▼
2. Calculate contracts   → calculateNumContracts({ tradeAmount, product, strikes, isBuy })
       │
       ▼
3. Calculate reserve     → calculateReservePrice(numContracts, mmPrice, spot, product)
       │
       ▼
4. Submit RFQ            → optionFactory.requestForQuotation(request)
```

### SELL Order Example

When selling, use `feeAdjustedBid` and supply collateral.

```typescript
import {
  ThetanutsClient,
  calculateNumContracts,
  calculateCollateralRequired,
  calculateReservePrice,
} from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453 });

// User sells PUT with 4000 USDC collateral
const tradeAmount = 4000;
const strike = 2000;
const spot = 2500;

const pricing = await client.mmPricing.getTickerPricing('ETH-16MAR24-2000-P');

const numContracts = calculateNumContracts({
  tradeAmount,
  product: 'PUT',
  strikes: [strike],
  isBuy: false,
});
// Result: 2 contracts (4000 / 2000)

const collateral = calculateCollateralRequired(numContracts, 'PUT', [strike]);
// Result: 4000 USDC

const reservePrice = calculateReservePrice(
  numContracts,
  pricing.feeAdjustedBid,  // Use bid for selling
  spot,
  'PUT'
);
// Result: minimum premium to receive (in USDC)

console.log(`Selling ${numContracts} contracts`);
console.log(`Collateral required: ${collateral} USDC`);
console.log(`Reserve price: ${reservePrice} USDC`);
```

### BUY Order Example

When buying, use `feeAdjustedAsk` and specify a premium budget.

```typescript
import {
  ThetanutsClient,
  calculateNumContracts,
  calculateReservePrice,
} from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453 });

const budget = 200;  // USDC to spend on premium
const strike = 2000;
const spot = 2000;

const pricing = await client.mmPricing.getTickerPricing('ETH-16MAR24-2000-P');

const numContracts = calculateNumContracts({
  tradeAmount: budget,
  product: 'PUT',
  strikes: [strike],
  isBuy: true,
  mmPrice: pricing.feeAdjustedAsk,  // Use ask for buying
  spot,
});
// Result: 2 contracts (200 / 100 premium per contract)

const reservePrice = calculateReservePrice(
  numContracts,
  pricing.feeAdjustedAsk,
  spot,
  'PUT'
);
// Result: maximum premium to pay (in USDC)

console.log(`Buying ${numContracts} contracts`);
console.log(`Reserve price: ${reservePrice} USDC`);
```

### BUY vs SELL Summary

|                        | SELL Order                               | BUY Order                          |
| ---------------------- | ---------------------------------------- | ---------------------------------- |
| Input amount           | Collateral to post                       | Premium budget                     |
| MM price to use        | `feeAdjustedBid`                         | `feeAdjustedAsk`                   |
| `numContracts` formula | `tradeAmount / maxLoss`                  | `tradeAmount / premiumPerContract` |
| `reservePrice` meaning | Minimum premium to receive               | Maximum premium to pay             |
| Collateral required    | Yes — from `calculateCollateralRequired` | No (buyer)                         |

***

## See Also

* [MM Pricing Overview](/sdk/pricing/mm-pricing) - `getAllPricing`, `getTickerPricing`, fee adjustment formula
* [Collateral Cost Reference](/sdk/pricing/collateral-cost) - APR rates and carrying cost formula
* [Filters & Utilities](/sdk/pricing/filters-utilities) - Filter and sort helpers for pricing arrays


# Filters & Utilities

Helper methods on `MMPricingModule` for filtering, sorting, and selecting pricing data from a pricing array.

## Setup

All utilities are methods on `client.mmPricing`. Start by fetching a pricing array:

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453 });

const all = await client.mmPricing.getAllPricing('ETH');
const values = Object.values(all);
```

***

## filterExpired

Removes options whose expiry has already passed.

```typescript
const active = client.mmPricing.filterExpired(values);
// Returns only options with expiry > Date.now()
```

***

## sortByExpiryAndStrike

Sorts options by nearest expiry first, then ascending strike within each expiry.

```typescript
const sorted = client.mmPricing.sortByExpiryAndStrike(values);
// [{ expiry: 1710547200, strike: 1800 }, { expiry: 1710547200, strike: 2000 }, ...]
```

***

## getUniqueExpiries

Returns a deduplicated list of expiry date strings in `YYYY-MM-DD` format, sorted ascending.

```typescript
const expiries = client.mmPricing.getUniqueExpiries(values);
// ['2025-02-16', '2025-03-16', '2025-06-27', ...]
```

***

## filterByType

Filters the array to only calls (`isCall: true`) or only puts (`isCall: false`).

```typescript
const puts  = client.mmPricing.filterByType(values, false);
const calls = client.mmPricing.filterByType(values, true);
```

***

## filterByExpiry

Filters the array to options expiring on a specific date string (`YYYY-MM-DD`).

```typescript
const feb16 = client.mmPricing.filterByExpiry(values, '2025-02-16');
```

***

## filterByStrikeRange

Filters the array to options whose strike falls within `[min, max]` (inclusive).

```typescript
const nearATM = client.mmPricing.filterByStrikeRange(values, 1800, 2200);
```

***

## getPricingArray

Convenience method: fetches all pricing for an underlying, removes expired entries, and returns the result as a sorted array in one call.

```typescript
const pricing = await client.mmPricing.getPricingArray('ETH');
// Equivalent to:
// const all = await client.mmPricing.getAllPricing('ETH');
// const sorted = client.mmPricing.sortByExpiryAndStrike(
//   client.mmPricing.filterExpired(Object.values(all))
// );
```

***

## Combining Utilities

Utilities can be chained to build a precise subset. Example: active put options near ATM for a specific expiry.

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453 });

const pricing = await client.mmPricing.getPricingArray('ETH');

const nearATMPuts = client.mmPricing.filterByStrikeRange(
  client.mmPricing.filterByType(
    client.mmPricing.filterByExpiry(pricing, '2025-03-16'),
    false  // puts only
  ),
  1800,
  2200
);

console.log(nearATMPuts.map(p => p.strike));
// [1800, 1900, 2000, 2100, 2200]
```

***

## See Also

* [MM Pricing Overview](/sdk/pricing/mm-pricing) - `getAllPricing`, `getTickerPricing`, and `MMPricingModule` method table
* [Position & Spread Pricing](/sdk/pricing/position-spread-pricing) - `getPositionPricing` and `getSpreadPricing`
* [Collateral Cost Reference](/sdk/pricing/collateral-cost) - APR rates and carrying cost formula


# Collateral Cost Reference

Understand how collateral carrying cost is calculated, what APR rates apply per asset, and how it affects the pricing of short option positions.

## Key Concepts

| Concept             | Description                                                           |
| ------------------- | --------------------------------------------------------------------- |
| **Reserve Price**   | The max (BUY) or min (SELL) acceptable total premium per contract     |
| **Collateral**      | Funds locked by the seller to secure the option; pulled at settlement |
| **Collateral Cost** | Opportunity cost of locked capital: `amount × APR × time`             |

***

## APR Rates

The SDK uses fixed annual percentage rates per collateral type:

| Collateral  | APR | Example                                      |
| ----------- | --- | -------------------------------------------- |
| BTC (cbBTC) | 1%  | 1 BTC locked for 1 year costs 0.01 BTC       |
| ETH (WETH)  | 4%  | 1 ETH locked for 30 days costs ≈ 0.00329 ETH |
| USD (USDC)  | 7%  | $2000 locked for 30 days costs ≈ $11.51      |

```typescript
import { COLLATERAL_APR } from '@thetanuts-finance/thetanuts-client';

console.log(COLLATERAL_APR);
// { BTC: 0.01, ETH: 0.04, USD: 0.07 }
```

***

## Collateral Cost Formula

```
collateralCost = collateralValue × APR × timeToExpiry
```

Where `timeToExpiry` is expressed in years (e.g., 30 days = `30 / 365`).

### Vanilla Option Example

```typescript
import { calculateCollateralCost } from '@thetanuts-finance/thetanuts-client';

// PUT option: 1 contract at strike 2000, 30 days to expiry, USDC collateral
const collateralUsd = 2000;
const timeToExpiryYears = 30 / 365;
const collateralType = 'USD';

const cost = calculateCollateralCost(collateralUsd, timeToExpiryYears, collateralType);
// cost = 2000 × 0.07 × (30/365) = $11.51
```

### Spread Collateral Cost

For spreads, the carrying cost is based on the **spread width** (max loss), not the sum of individual leg costs.

```typescript
import { calculateSpreadCollateralCost } from '@thetanuts-finance/thetanuts-client';

// CALL_SPREAD [2000, 2500]: width = $500
const widthUsd = 500;
const timeToExpiryYears = 30 / 365;

const spreadCC = calculateSpreadCollateralCost(widthUsd, timeToExpiryYears);
// spreadCC = 500 × 0.07 × (30/365) = $2.88
```

***

## How Carrying Cost Affects Pricing

Collateral cost is only relevant for **short positions** (sellers). It is subtracted from the bid price, representing the minimum premium a seller needs to cover their cost of capital.

| Price Direction  | Formula                                   |
| ---------------- | ----------------------------------------- |
| Ask (long/buy)   | `rawAsk + feeAdjustment`                  |
| Bid (short/sell) | `rawBid - feeAdjustment - collateralCost` |

The `byCollateral` breakdown on ticker pricing exposes per-collateral MM prices:

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453 });

const pricing = await client.mmPricing.getTickerPricing('ETH-16FEB26-1800-P');

// Fee-adjusted prices (no collateral cost applied)
console.log(pricing.feeAdjustedBid);
console.log(pricing.feeAdjustedAsk);

// MM prices per collateral type (collateral cost factored in)
console.log(pricing.byCollateral.USD.mmBidPrice);  // Lower than feeAdjustedBid
console.log(pricing.byCollateral.USD.mmAskPrice);  // Same as feeAdjustedAsk (buyers don't pay CC)
console.log(pricing.byCollateral.ETH.mmBidPrice);
console.log(pricing.byCollateral.ETH.mmAskPrice);
```

### Position-Level Collateral Cost

`getPositionPricing` returns the carrying cost as an explicit field:

```typescript
const position = await client.mmPricing.getPositionPricing({
  ticker: 'ETH-16FEB26-1800-P',
  isLong: false,          // Short = seller pays collateral cost
  numContracts: 5,
  collateralToken: 'USDC',
});

console.log(position.basePremium);     // Fee-adjusted price only
console.log(position.collateralCost);  // Carrying cost component
console.log(position.totalPrice);      // basePremium - collateralCost (for short)
```

***

## Collateral by Product Type

Different products lock different collateral amounts. The max loss per contract determines the collateral requirement.

| Product                    | Collateral Locked (per contract)            |
| -------------------------- | ------------------------------------------- |
| PUT                        | `strike` USDC                               |
| INVERSE\_CALL              | `1` WETH (or cbBTC for BTC)                 |
| LINEAR\_CALL               | `strike` USDC                               |
| CALL\_SPREAD / PUT\_SPREAD | `upperStrike - lowerStrike` USDC            |
| IRON\_CONDOR               | `max(putSpreadWidth, callSpreadWidth)` USDC |

For collateral calculations:

```typescript
import { calculateCollateralRequired } from '@thetanuts-finance/thetanuts-client';

// PUT: 5 contracts at strike 2000 = 10000 USDC
calculateCollateralRequired(5, 'PUT', [2000]);
// Result: 10000

// CALL_SPREAD: 10 contracts, strikes [2000, 2500] = 5000 USDC
calculateCollateralRequired(10, 'CALL_SPREAD', [2000, 2500]);
// Result: 5000 (10 × 500 width)

// INVERSE_CALL: 10 contracts = 10 WETH
calculateCollateralRequired(10, 'INVERSE_CALL', [2000]);
// Result: 10
```

***

## See Also

* [MM Pricing Overview](/sdk/pricing/mm-pricing) - Fee adjustment formula and `MMPricingModule` methods
* [Position & Spread Pricing](/sdk/pricing/position-spread-pricing) - `getPositionPricing` and `getSpreadPricing` with collateral cost
* [Filters & Utilities](/sdk/pricing/filters-utilities) - Filter and sort helpers for pricing arrays


# Overview — Pick Your Route

Thetanuts ships two agent-facing packages on top of the SDK. Both are thin layers over the same [`@thetanuts-finance/thetanuts-client`](https://www.npmjs.com/package/@thetanuts-finance/thetanuts-client) encode helpers — the difference is **who signs the transaction and where the safety boundary lives**.

## The two packages

|                 | [`@thetanuts-finance/mcp`](https://www.npmjs.com/package/@thetanuts-finance/mcp)                                    | [`@thetanuts-finance/agentkit`](https://www.npmjs.com/package/@thetanuts-finance/agentkit) |
| --------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| What it is      | MCP server: \~100 tools an LLM client calls over the Model Context Protocol                                         | Coinbase AgentKit `ActionProvider` library you embed in your own agent code                |
| Who signs       | **Never signs.** Pairs with a signer — Base MCP (you approve each tx in Base Account), Safe, or a CDP policy wallet | **The agent's own wallet** (CDP, viem, Privy server wallets), unattended                   |
| Runs in         | Claude Desktop, Claude Code, Cursor, ChatGPT, Codex — any MCP client                                                | Your backend agent process (LangChain, Vercel AI SDK), or as its own MCP server            |
| Safety boundary | Outside the LLM: wallet approval UI or signer policy                                                                | In code: fail-closed `SafetyPolicy` (notional caps, collateral allowlist, host hook)       |
| Use when        | Human-in-the-loop chat trading; maximum client reach                                                                | Headless trading bots, MM bots, custodied agent vaults                                     |

This split is deliberate: the MCP server can guarantee it **cannot move funds** (it holds no keys and builds calldata only), while autonomous signing stays an explicit, separately-installed opt-in. The two can run side by side.

## The three routes

| You want                                                  | You run                                                                                                                                     | Guide                                             |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
| **Trade from chat, approving every transaction yourself** | `@thetanuts-finance/mcp` + [Base MCP](https://docs.base.org/ai-agents/quickstart) — calldata is prepared, you click approve in Base Account | [Trade from Chat](/sdk/ai-agents/base-mcp-plugin) |
| **Trade from chat, but the agent signs by itself**        | `@thetanuts-finance/agentkit` run as an MCP server (Coinbase's MCP adapter + a CDP wallet under `SafetyPolicy` caps)                        | [AgentKit](/sdk/ai-agents/agentkit)               |
| **A fully headless bot — no chat client at all**          | `@thetanuts-finance/agentkit` embedded in your own code (LangChain, Vercel AI SDK)                                                          | [AgentKit](/sdk/ai-agents/agentkit)               |

If you're unsure, take the first route — a transaction can never leave your wallet without your click.

## Reads only?

If you just want an LLM that can *read* the protocol (markets, positions, IV, Greeks) and help you write SDK code, the MCP server alone is enough — no wallet, no signer. See [MCP Server](/sdk/ai-agents/mcp-server), or skip servers entirely with the copy-paste [LLM Context](/sdk/resources/llm-context) prompt.

## Deeper material

* [MCP Server guide](/sdk/ai-agents/mcp-server) — every tool, environment variables, the full comparison
* [Trade from Chat](/sdk/ai-agents/base-mcp-plugin) — the Base MCP plugin setup
* [AgentKit guide](/sdk/ai-agents/agentkit) — autonomous agents, both modes
* AgentKit repo: [SETUP.md](https://github.com/Thetanuts-Finance/thetanuts-agentkit/blob/main/SETUP.md) (end-to-end setup: CDP wallet, client configs for Claude Desktop/Code, Cursor, Codex) and [SKILL.md](https://github.com/Thetanuts-Finance/thetanuts-agentkit/blob/main/SKILL.md) (a walkthrough skill any skill-aware agent can load)


# MCP Server

An MCP (Model Context Protocol) server that exposes Thetanuts SDK functionality to AI agents and LLM-powered tools.

The server reads protocol state and builds transaction calldata. It does not hold wallet private keys, sign wallet transactions, or broadcast transactions itself; `prepare_*` tools return wallet-signable call bundles.

For full details, see [mcp-server/README.md](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/mcp-server/README.md) and [mcp-server/SPEC.md](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/mcp-server/SPEC.md).

> **Tip for LLM users:** if you don't need a persistent MCP connection, you can paste a one-line prompt into Claude or Cursor and have your LLM fetch the SDK context directly. See [LLM Context](/sdk/resources/llm-context) for the copy-paste prompt.

## This MCP vs `@thetanuts-finance/agentkit`

> Full landscape, including the trade-from-chat and headless-bot routes: [AI Agents — Pick Your Route](/sdk/ai-agents/agents-overview).

Both are thin layers over the same `@thetanuts-finance/thetanuts-client` encode helpers — the difference is who signs and where the safety boundary lives:

|                 | `@thetanuts-finance/mcp`                                                                 | [`@thetanuts-finance/agentkit`](https://github.com/Thetanuts-Finance/thetanuts-agentkit) |
| --------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| What it is      | MCP server for chat clients (Claude Desktop, Cursor, ChatGPT)                            | Coinbase AgentKit `ActionProvider` library for your own agent code                       |
| Who signs       | Never signs — pair with Base MCP (you approve each tx in Base Account) or another signer | The agent's own wallet (CDP, viem, Privy), unattended                                    |
| Safety boundary | Outside the LLM: wallet approval UI or signer policy                                     | In code: fail-closed `SafetyPolicy` (notional caps, collateral allowlist)                |
| Use when        | Human-in-the-loop chat trading                                                           | Headless trading bots, MM bots, custodied agent vaults                                   |

For an MCP server that *does* sign by itself, see the agentkit repo's [`examples/mcp-server-quickstart.ts`](https://github.com/Thetanuts-Finance/thetanuts-agentkit/blob/main/examples/mcp-server-quickstart.ts) — it runs the ActionProvider as an autonomous-signing MCP server via Coinbase's official MCP adapter. It is deliberately a separate artifact, so this server keeps the guarantee that it can never move funds.

## Available Tools

### LLM Context Tools (call these first)

If you're an LLM connecting for the first time, call `get_sdk_context` once and cache the result for the session — it returns the full embedded SDK context (every module, key types, common workflows, gotchas) in \~35 KiB of markdown. Same content as the [`llms-full.txt`](https://raw.githubusercontent.com/Thetanuts-Finance/thetanuts-sdk/main/llms-full.txt) at the repo root.

| Tool                    | Description                                                                              |
| ----------------------- | ---------------------------------------------------------------------------------------- |
| `get_sdk_context`       | Full long-form SDK context. Call this first — covers every module and the common gotchas |
| `get_sdk_context_index` | Curated index of canonical SDK docs (llmstxt.org spec) — links only                      |
| `get_sdk_context_size`  | Byte size of the embedded context — use to budget before fetching                        |

These three handlers run before the chain client is initialized, so they don't consume an RPC call.

### Indexer API (OptionBook data)

| Tool                 | Description                             |
| -------------------- | --------------------------------------- |
| `get_stats`          | Protocol statistics                     |
| `get_user_positions` | User's option positions                 |
| `get_user_history`   | User's trade history                    |
| `get_referrer_stats` | Aggregated stats for a referrer address |

### State / RFQ API

| Tool            | Description            |
| --------------- | ---------------------- |
| `get_rfq`       | Get specific RFQ by ID |
| `get_user_rfqs` | User's RFQ history     |

### Market & Orders

| Tool                | Description                             |
| ------------------- | --------------------------------------- |
| `get_market_data`   | Current prices for BTC, ETH, SOL, etc.  |
| `get_market_prices` | All supported asset prices              |
| `fetch_orders`      | All orders from the order book          |
| `filter_orders`     | Filter orders by asset, type, or expiry |

### Token & Option Data

| Tool                      | Description                                   |
| ------------------------- | --------------------------------------------- |
| `get_token_balance`       | Token balance for address                     |
| `get_token_allowance`     | Token allowance for spender                   |
| `get_token_info`          | Token decimals and symbol                     |
| `get_option_info`         | Option contract details                       |
| `get_full_option_info`    | All strikes, positions, and settlement status |
| `calculate_option_payout` | Calculate payout at settlement price          |
| `preview_fill_order`      | Dry-run order fill preview                    |

### MM Pricing

| Tool                       | Description                                    |
| -------------------------- | ---------------------------------------------- |
| `get_mm_all_pricing`       | All MM-adjusted pricing for an asset           |
| `get_mm_ticker_pricing`    | MM pricing for a specific ticker               |
| `get_mm_position_pricing`  | Position-aware MM pricing with collateral cost |
| `get_mm_spread_pricing`    | MM pricing for a two-leg spread                |
| `get_mm_condor_pricing`    | MM pricing for a four-leg condor               |
| `get_mm_butterfly_pricing` | MM pricing for a three-leg butterfly           |

### RFQ Quotation Tools

| Tool                       | Description                                          |
| -------------------------- | ---------------------------------------------------- |
| `get_quotation`            | Quotation info by ID including parameters and state  |
| `get_quotation_count`      | Total quotations created                             |
| `get_user_offers`          | All RFQ offers made by a user                        |
| `get_user_options`         | All options held by a user                           |
| `prepare_settle_rfq`       | Build settlement call after reveal phase             |
| `prepare_settle_rfq_early` | Build early-settlement call before offer period ends |
| `prepare_cancel_rfq`       | Build RFQ cancellation call                          |
| `prepare_cancel_offer`     | Build offer cancellation call                        |

### RFQ Builder Tools

| Tool                            | Description                                                                 |
| ------------------------------- | --------------------------------------------------------------------------- |
| `prepare_suggest_reserve_price` | Suggest a per-contract reserve price from the live IV surface               |
| `prepare_request_rfq`           | Auth-gated RFQ creation call bundle with product-specific strike validation |

### Calculation Tools

| Tool                             | Description                                      |
| -------------------------------- | ------------------------------------------------ |
| `calculate_payout`               | Calculate payoff for structures                  |
| `calculate_num_contracts`        | Number of contracts from trade amount            |
| `calculate_collateral_required`  | Collateral required for a position in USDC       |
| `calculate_premium_per_contract` | Premium per contract in USD from MM price        |
| `calculate_reserve_price`        | Total reserve price (minimum acceptable premium) |
| `calculate_delivery_amount`      | Delivery amount for physical options             |
| `calculate_protocol_fee`         | Protocol fee for an RFQ trade                    |
| `convert_decimals`               | Convert to/from chain decimals                   |

### Validation Tools

| Tool                   | Description                                 |
| ---------------------- | ------------------------------------------- |
| `validate_butterfly`   | Validate butterfly strike configuration     |
| `validate_condor`      | Validate condor strike configuration        |
| `validate_iron_condor` | Validate iron condor strike configuration   |
| `validate_ranger`      | Validate ranger option strike configuration |

### Chain Configuration Tools

| Tool                             | Description                                        |
| -------------------------------- | -------------------------------------------------- |
| `get_chain_config`               | Chain contracts and tokens                         |
| `get_chain_config_by_id`         | Full chain configuration by chain ID               |
| `get_token_config_by_id`         | Token configuration by chain ID and symbol         |
| `get_option_implementation_info` | All implementation addresses and deployment status |

### Prepare Tools (Transaction Builders)

| Tool                                | Description                                        |
| ----------------------------------- | -------------------------------------------------- |
| `prepare_auth_challenge`            | Mint a single-use auth challenge                   |
| `prepare_approve`                   | Auth-gated OptionFactory collateral-token approval |
| `prepare_request_rfq`               | Build an RFQ creation call bundle                  |
| `prepare_suggest_reserve_price`     | Suggest a reserve price for `prepare_request_rfq`  |
| `prepare_make_offer`                | Encrypt an offer and return typed data to sign     |
| `prepare_make_offer_with_signature` | Build the make-offer call after signing            |

### Event Query Tools

| Tool                             | Description                                 |
| -------------------------------- | ------------------------------------------- |
| `get_order_fill_events`          | Historical fill events                      |
| `get_option_created_events`      | Historical option creation events           |
| `get_quotation_requested_events` | Historical RFQ request events               |
| `get_quotation_settled_events`   | Historical RFQ settlement events            |
| `get_position_closed_events`     | Position close events for a specific option |

### Utility Tools

| Tool                       | Description                                                      |
| -------------------------- | ---------------------------------------------------------------- |
| `parse_ticker`             | Parse option ticker (e.g., "ETH-16FEB26-1800-P") into components |
| `build_ticker`             | Build option ticker from components                              |
| `get_position_info`        | Position information for buyer or seller                         |
| `generate_example_keypair` | Generate example ECDH keypair (demo only)                        |

### Ranger Tools (RangerOption — zone-bound 4-strike payoff)

| Tool                                   | Description                                                            |
| -------------------------------------- | ---------------------------------------------------------------------- |
| `get_ranger_info`                      | Full state of a Ranger position (buyer, seller, strikes, zone, expiry) |
| `get_ranger_zone`                      | Inner zone bounds where the buyer earns max payout                     |
| `get_ranger_spread_width`              | Per-leg spread width (s2-s1 == s4-s3)                                  |
| `get_ranger_twap`                      | Current TWAP from the option's price-feed consumer                     |
| `calculate_ranger_payout`              | On-chain payout at a specific settlement price                         |
| `simulate_ranger_payout`               | Simulate payout for hypothetical strikes/numContracts (pure)           |
| `calculate_ranger_required_collateral` | Required collateral for given strikes + numContracts                   |

### Loan Tools (Non-liquidatable lending)

| Tool                        | Description                                                               |
| --------------------------- | ------------------------------------------------------------------------- |
| `get_lending_opportunities` | Fetch unfilled loan limit orders from the loan indexer                    |
| `get_loan_request`          | On-chain state for a specific loan quotation                              |
| `get_user_loans`            | All loans for an address from the loan indexer                            |
| `get_loan_option_info`      | Details for a loan-issued option (strike, expiry, collateral, underlying) |
| `is_loan_option_itm`        | Whether a loan-issued option is currently in-the-money                    |
| `fetch_loan_pricing`        | Deribit-style option pricing (30s cache)                                  |
| `get_loan_strike_options`   | Filtered strike options grouped by expiry                                 |

### WheelVault Tools (Ethereum mainnet — chainId 1)

The current MCP process is pinned to Base (`chainId 8453`), so WheelVault tools throw `NETWORK_UNSUPPORTED` here. Use the TypeScript SDK with `chainId: 1` for Ethereum WheelVault access.

| Tool                           | Description                                                      |
| ------------------------------ | ---------------------------------------------------------------- |
| `get_wheel_vault_state`        | Full state of a WheelVault series (balances, shares, last price) |
| `get_wheel_vault_series`       | Raw on-chain series struct                                       |
| `get_wheel_vault_series_count` | Total number of series in a WheelVault                           |
| `preview_wheel_deposit`        | Pre-flight: expected shares minted for a paired deposit          |
| `preview_wheel_withdraw`       | Pre-flight: expected base/quote returned for a share redemption  |
| `get_wheel_depth_chart`        | Depth-chart data across IV buckets                               |
| `get_wheel_buyer_options`      | Options held by a buyer (paginated via fromId/maxCount)          |
| `get_wheel_seller_positions`   | Seller exposures within a series                                 |
| `get_wheel_claimable_summary`  | Aggregate claimable amounts across multiple series               |

### StrategyVault Tools (Base — Fixed-strike + CLVEX vaults)

| Tool                               | Description                                                    |
| ---------------------------------- | -------------------------------------------------------------- |
| `get_strategy_vault_state`         | Full vault state (assets, shares, next expiry, recovery state) |
| `get_strategy_vault_total_assets`  | Base + quote assets currently held                             |
| `get_strategy_vault_share_balance` | A user's share balance in a vault                              |
| `get_strategy_vault_next_expiry`   | Next option-creation expiry timestamp                          |
| `can_strategy_vault_create_option` | Whether `createOption()` is currently eligible                 |
| `is_strategy_vault_recovery_mode`  | Whether the vault is paused for emergency withdrawals          |
| `get_all_strategy_vaults`          | Live state of every fixed-strike + CLVEX vault                 |
| `get_fixed_strike_vaults`          | Live state of fixed-strike vaults only                         |
| `get_clvex_vaults`                 | Live state of CLVEX directional/condor vaults only             |

## Setup

### Build

```bash
cd mcp-server
npm install
npm run build
```

### Claude Desktop

Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "thetanuts": {
      "command": "node",
      "args": ["/path/to/thetanuts-sdk/mcp-server/dist/index.js"],
      "env": {
        "THETANUTS_RPC_URL": "https://mainnet.base.org",
        "KEYSTORE_MASTER_KEY": "<32-byte-hex-for-prepare-tools>"
      }
    }
  }
}
```

### Other MCP Clients

Add to `.mcp.json` at your project root:

```json
{
  "mcpServers": {
    "thetanuts": {
      "command": "node",
      "args": ["./mcp-server/dist/index.js"]
    }
  }
}
```

### Development (no build required)

```bash
cd mcp-server
npm run dev
```

## Environment Variables

| Variable                  | Default                                  | Description                                                                   |
| ------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------- |
| `THETANUTS_RPC_URL`       | `https://mainnet.base.org`               | Base RPC endpoint; this MCP currently constructs the SDK with `chainId: 8453` |
| `KEYSTORE_MASTER_KEY`     | required for `prepare_*` RFQ write tools | 32-byte hex key used to encrypt the local RFQ ECDH keystore                   |
| `THETANUTS_KEYSTORE_PATH` | `~/.thetanuts/mcp-keystore.sqlite`       | Optional path for the encrypted RFQ keystore                                  |

## Example Calls

### Get market prices

```
Tool: get_market_data
Result: { prices: { BTC: 95000, ETH: 3200, ... } }
```

### Filter ETH call orders

```
Tool: filter_orders
Args: { asset: "ETH", type: "call" }
Result: { count: 5, orders: [...] }
```

### Get MM pricing for a specific ticker

```
Tool: get_mm_ticker_pricing
Args: { ticker: "ETH-28FEB26-2800-C" }
Result: { rawBidPrice: 0.0245, rawAskPrice: 0.0255, feeAdjustedBid: 0.0241, ... }
```

### Prepare a settlement call (returns calldata, does not send)

```
Tool: prepare_settle_rfq
Args: { quotationId: "744" }
Result: { chain: "base", calls: [{ to: "0x...", data: "0x...", value: "0x0" }] }
```

## See Also

* [mcp-server/README.md](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/mcp-server/README.md) — full setup instructions and examples
* [mcp-server/SPEC.md](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/mcp-server/SPEC.md) — tool parameter schemas
* [Error Handling](/sdk/guides/error-handling) — error codes used by the underlying SDK


# Trade from Chat (Base MCP Plugin)

The Base MCP plugin lets anyone trade Thetanuts options from a chat client (Claude Desktop, Claude Code, Cursor, ChatGPT, Codex) **while keeping every signature in their own hands**. The LLM prepares the trade; you approve it in [Base Account](https://docs.base.org/ai-agents/quickstart). The plugin never signs, never broadcasts, never holds keys.

Source: [`mcp-server/plugins/base-mcp/`](https://github.com/Thetanuts-Finance/thetanuts-sdk/tree/main/mcp-server/plugins/base-mcp) — it follows Base's [custom plugin spec](https://docs.base.org/ai-agents/plugins/custom-plugins).

## How it works

Two MCP servers split the job — protocol knowledge and key custody never share a process:

```
┌─────────────┐   prepare_*    ┌──────────────────────────┐    encode*    ┌──────────────┐
│ LLM (Claude │ ─────────────▶ │ Thetanuts MCP (stdio)    │ ────────────▶ │ Thetanuts    │
│  + Base MCP)│                │ v1.0.0+                  │               │ SDK helpers  │
│             │ ◀───────────── │ → { chain, calls[] }     │               │              │
└──────┬──────┘   unsigned tx  └──────────────────────────┘               └──────────────┘
       │
       │ send_calls
       ▼
┌─────────────┐
│ Base Account│ → user approval → tx broadcast on Base 8453
└─────────────┘
```

The Thetanuts MCP's `prepare_*` tools return Base-MCP-ready `{ chain, calls }` envelopes; the LLM hands them to Base MCP's `send_calls`; you review and confirm in Base Account.

## Install

1. **Base MCP** in your client — see the [Base quickstart](https://docs.base.org/ai-agents/quickstart).
2. **Thetanuts MCP** (v1.0.0+):

   ```bash
   claude mcp add thetanuts-mcp \
     -e KEYSTORE_MASTER_KEY="$(openssl rand -hex 32)" \
     -- npx -y @thetanuts-finance/mcp
   ```

   (Equivalent config-file entries for Claude Desktop / Cursor / Codex.)
3. **The plugin skill**:

   ```bash
   npx skills add Thetanuts-Finance/thetanuts-sdk \
     --skill mcp-server/plugins/base-mcp \
     -a claude-code        # or: cursor | codex | hermes
   ```

   For Claude Desktop / claude.ai or ChatGPT, zip the plugin directory and upload it as a custom skill.
4. A funded Base Account on Base mainnet (USDC for collateral/premium, a little ETH for gas).

## What it can do

RFQ is the only write path: request quotes (all 9 products — puts, calls, spreads, butterflies, condors, iron condors), make sealed-bid offers, settle (normal or early), cancel, and standalone approvals. OptionBook fills are deliberately not surfaced — their silent-rejection failure modes (maker offline, indexer lag, race-loss) make poor first-trade UX in chat. Reads (orderbook, positions, IV surface, pricing) come from the same Thetanuts MCP.

## RFQ keys, handled for you

Sealed-bid RFQs need an ECDH keypair so market makers can encrypt offers to you. The Thetanuts MCP manages this server-side: keys are derived per wallet, stored AES-256-GCM-encrypted in a local SQLite keystore rooted in your `KEYSTORE_MASTER_KEY`, and **never enter the LLM transcript**. These are encryption keys only — they cannot move funds.

## Out of scope (v1)

Vault deposits/withdrawals, Ethereum mainnet (chainId 1), physical multi-leg options, and loan flows.

## See also

* [AI Agents overview](/sdk/ai-agents/agents-overview) — how this route compares to autonomous AgentKit trading
* [MCP Server guide](/sdk/ai-agents/mcp-server) — the full tool reference behind this plugin


# AgentKit (Autonomous Agents)

[`@thetanuts-finance/agentkit`](https://www.npmjs.com/package/@thetanuts-finance/agentkit) is a [Coinbase AgentKit](https://docs.cdp.coinbase.com/agent-kit/welcome) `ActionProvider` for agents that **own their own wallet** and trade Thetanuts options unattended — request RFQs, receive encrypted sealed-bid offers, and settle, with no human approval per transaction.

It lives in a sibling repo: [`Thetanuts-Finance/thetanuts-agentkit`](https://github.com/Thetanuts-Finance/thetanuts-agentkit). This page is the orientation; the repo's [SETUP.md](https://github.com/Thetanuts-Finance/thetanuts-agentkit/blob/main/SETUP.md) is the step-by-step guide.

> **This is the autonomous route.** The agent's wallet signs by itself — the configured `SafetyPolicy` caps are the only brake. If you want to approve each trade in your own wallet, use the [Base MCP plugin](/sdk/ai-agents/base-mcp-plugin) instead. See [Pick Your Route](/sdk/ai-agents/agents-overview).

## Action surface

7 write actions + 3 read actions, each Zod-validated with LLM-friendly descriptions:

| Action                                               | Purpose                                                                        |
| ---------------------------------------------------- | ------------------------------------------------------------------------------ |
| `approve`                                            | ERC20 approval to the OptionFactory (auto-bundled for SELL RFQs)               |
| `request_rfq`                                        | Open an RFQ — puts, calls, spreads, butterflies, condors, iron condors         |
| `make_offer`                                         | Sealed-bid offer on someone's RFQ (EIP-712 signed, encrypted to the requester) |
| `settle_rfq` / `settle_rfq_early`                    | Settle after the window closes / accept a specific offer early                 |
| `cancel_rfq` / `cancel_offer`                        | Withdraw the agent's own RFQ / offer                                           |
| `get_user_positions`, `get_rfq`, `get_market_prices` | Reads                                                                          |

## The safety model

Every value-moving action passes a **fail-closed `SafetyPolicy`** — omit it and all writes throw `SAFETY_LIMITS_REQUIRED`:

```typescript
thetanutsActionProvider({
  safetyLimits: {
    maxNotionalUsdcPerAction: 50_000_000n, // $50 hard cap per action
    maxApprovalAmount: 'exact',            // never grant MAX_UINT256
    allowedCollateral: ['USDC'],
    // onWriteAction: (ctx) => 'allow' | 'reject'  — host audit/review hook
  },
})
```

Run it only with a **dedicated wallet** funded with what you're prepared to let an agent spend. The recommended wallet is a CDP server wallet (MPC — the agent never sees key material); viem and Privy providers also work.

## Two ways to consume it

**Embedded in your own bot** — LangChain or Vercel AI SDK, via Coinbase's framework adapters. Runnable quickstarts: [`examples/`](https://github.com/Thetanuts-Finance/thetanuts-agentkit/tree/main/examples) in the agentkit repo, plus a complete [covered-call premium hunter](https://github.com/Thetanuts-Finance/thetanuts-sdk/tree/main/examples/options-trading-agent) in this repo.

**As an autonomous-signing MCP server** — Coinbase's official [`@coinbase/agentkit-model-context-protocol`](https://docs.cdp.coinbase.com/agent-kit/core-concepts/model-context-protocol) adapter turns the ActionProvider into a stdio MCP server, so a chat client (Claude Desktop, Claude Code, Cursor, Codex) gets tools that sign on their own. Runnable server: [`examples/mcp-server-quickstart.ts`](https://github.com/Thetanuts-Finance/thetanuts-agentkit/blob/main/examples/mcp-server-quickstart.ts); client configs for all four clients are in [SETUP.md](https://github.com/Thetanuts-Finance/thetanuts-agentkit/blob/main/SETUP.md). This is deliberately a **separate server** from `@thetanuts-finance/mcp`, which never signs — the two can be installed side by side.

## Walkthrough skill

The repo ships a [SKILL.md](https://github.com/Thetanuts-Finance/thetanuts-agentkit/blob/main/SKILL.md) (`npx skills add Thetanuts-Finance/thetanuts-agentkit`, also inside the npm package) that teaches any skill-aware agent to walk a user through the kit — detection, setup, funding, a read-only demo, and a safe first trade.

## Requirements

* `@thetanuts-finance/thetanuts-client` **>= 0.3.0** — `make_offer` / `settle_rfq_early` use the `buildOfferTypedData`, `getRequesterPublicKey`, and `getOffer` APIs introduced in 0.3.0
* `@coinbase/agentkit` >= 0.10.0, `reflect-metadata`
* Base mainnet only (chainId 8453) — the provider rejects any other network


# Position Management

Query and manage option positions using the `client.option` module.

## Methods

| Method                                                     | Description                          | Signer |
| ---------------------------------------------------------- | ------------------------------------ | ------ |
| `getOptionInfo(address)`                                   | Get basic option details             | No     |
| `getFullOptionInfo(address)`                               | Get all info in a single call        | No     |
| `calculatePayout(address, price)`                          | Calculate payout at settlement price | No     |
| `calculateRequiredCollateral(address, strikes, contracts)` | Get collateral needed                | No     |
| `getStrikes(address)`                                      | Get strike prices                    | No     |
| `getExpiry(address)`                                       | Get expiry timestamp                 | No     |
| `isExpired(address)`                                       | Check if expired                     | No     |
| `isSettled(address)`                                       | Check if settled                     | No     |
| `getBuyer(address)`                                        | Get buyer address                    | No     |
| `getSeller(address)`                                       | Get seller address                   | No     |
| `getNumContracts(address)`                                 | Get contract count                   | No     |
| `getCollateralAmount(address)`                             | Get collateral amount                | No     |
| `close(address)`                                           | Close position                       | Yes    |
| `transfer(address, isBuyer, target)`                       | Transfer buyer or seller role        | Yes    |
| `split(address, amount)`                                   | Split position into two              | Yes    |
| `payout(address)`                                          | Execute payout after expiry          | Yes    |

## Usage

### Get Full Option Info

`getFullOptionInfo()` batches all state into a single RPC call. Use it when you need more than one field.

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider });

const info = await client.option.getFullOptionInfo(optionAddress);

console.log(info.info);             // { optionType, strikes, expiryTimestamp, collateralToken, priceFeed, implementation }
console.log(info.buyer);            // '0x...'
console.log(info.seller);           // '0x...'
console.log(info.isExpired);        // false
console.log(info.isSettled);        // false
console.log(info.numContracts);     // bigint
console.log(info.collateralAmount); // bigint
```

> `getFullOptionInfo()` returns nullable fields (`| null`) for proxy contracts with incompatible ABI versions so it returns partial data instead of throwing.

### Individual State Reads

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider });

const info      = await client.option.getOptionInfo(optionAddress);
const buyer     = await client.option.getBuyer(optionAddress);
const seller    = await client.option.getSeller(optionAddress);
const expired   = await client.option.isExpired(optionAddress);
const settled   = await client.option.isSettled(optionAddress);
const contracts = await client.option.getNumContracts(optionAddress);
const collateral = await client.option.getCollateralAmount(optionAddress);
```

### Calculate Payout

`calculatePayout()` accepts the settlement price in 8-decimal format (`$2000 = 200000000000n`).

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider });

const payout = await client.option.calculatePayout(
  optionAddress,
  200000000000n  // $2000 in 8 decimals
);
```

For off-chain payoff diagram data, use `client.utils.calculatePayout()` instead, which runs locally without RPC calls:

```typescript
const payout = client.utils.calculatePayout({
  type: 'put',
  strikes: [200000000000n],       // 8 decimals
  settlementPrice: 190000000000n,
  numContracts: 1000000000000000000n, // 18 decimals
});
```

#### simulatePayout (current spot)

When you want the current expected payout without supplying a price, use `simulatePayout()`. It reads the option's TWAP from the on-chain consumer and runs `calculatePayout` against it. Pass an explicit `price` to override.

```typescript
// Use the current TWAP
const live = await client.option.simulatePayout(optionAddress);

// Or override with an arbitrary price
const stress = await client.option.simulatePayout(optionAddress, 180000000000n);
```

### Close a Position

Both buyer and seller can close a position before expiry if both agree (bilateral close). Requires a signer.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(privateKey, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

const result = await client.option.close(optionAddress);
console.log('Close tx:', result.hash);
```

### Execute Payout

After expiry, either party can call `payout()` to settle the option and distribute proceeds.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(privateKey, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

const expired = await client.option.isExpired(optionAddress);
if (expired) {
  const result = await client.option.payout(optionAddress);
  console.log('Payout tx:', result.hash);
}
```

### Transfer a Position

Transfer the buyer or seller role to a new address.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(privateKey, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

// Transfer buyer role to new address
const result = await client.option.transfer(
  optionAddress,
  true,           // true = transfer buyer role, false = seller role
  '0xNewOwner'
);
```

### Split a Position

Split one option contract into two separate contracts. Useful for partial exits.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(privateKey, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

const splitCollateralAmount = 500_000000n; // 500 USDC
const result = await client.option.split(optionAddress, splitCollateralAmount);
```

## Return Types

### FullOptionInfo

```typescript
interface FullOptionInfo {
  info: OptionInfo;
  buyer: string;
  seller: string;
  isExpired: boolean;
  isSettled: boolean;
  numContracts: bigint;
  collateralAmount: bigint;
}

interface OptionInfo {
  optionType: bigint;
  strikes: bigint[];
  expiryTimestamp: bigint;
  collateralToken: string;
  priceFeed: string;
  implementation: string;
}
```

## See Also

* [Token Operations](/sdk/guides/token-operations) — approve collateral before close or payout
* [Events](/sdk/guides/events) — query `getPositionClosedEvents` for settlement history
* [Error Handling](/sdk/guides/error-handling) — `NotExpired`, `AlreadySettled`, `NotBuyer` revert reasons


# Ranger Options

Ranger is a 4-strike, zone-bound payoff that pays the buyer the maximum amount when the settlement price lands inside a configured zone, with linearly decaying payouts outside that zone. Available on Base mainnet via the `client.ranger` module (Base\_r12 onwards).

## When you'd use a Ranger

* You expect the underlying to **stay within a range** at expiry (e.g., ETH between $2,300 and $2,500).
* A vanilla call/put or even a condor is too narrow — you want a clean way to express a range view with a single trade.
* You're comfortable with the seller side too: collecting premium when you expect the price to **break out** of the zone.

## Anatomy

A Ranger has 4 strikes: `[s1, s2, s3, s4]`.

* The **zone** is bounded by `s2` and `s3` — that's where the buyer earns the max payout.
* Outside the zone, the payout decays linearly toward zero between `s1`-to-`s2` and `s3`-to-`s4`.
* Below `s1` or above `s4`, the payout is zero.

The contract enforces equal **spread widths**: `s2 - s1 == s4 - s3`. This is what makes the payoff symmetric around the zone.

## Quick reference

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider, signer });

// Read a Ranger option's full state
const info = await client.ranger.getInfo(rangerAddress);
// {
//   buyer, seller, creator,
//   collateralToken, collateralAmount, numContracts, expiryTimestamp,
//   chainlinkPriceFeed, strikes,
//   zone: { zoneLower, zoneUpper },
//   spreadWidth,
//   optionSettled,
// }

// Read just the zone bounds
const { zoneLower, zoneUpper } = await client.ranger.getZone(rangerAddress);

// Read the per-leg spread width
const width = await client.ranger.getSpreadWidth(rangerAddress);

// Simulate the payout at a hypothetical settlement price
const payout = await client.ranger.simulatePayout(
  rangerAddress,
  client.utils.toPriceDecimals(2400),  // settlement price (8 decimals)
  await client.ranger.getStrikes(rangerAddress),
  await client.ranger.getInfo(rangerAddress).then(i => i.numContracts),
);

// On-chain payout at an exact settlement price (no off-chain math)
const exact = await client.ranger.calculatePayout(rangerAddress, settlementPrice);

// Current TWAP from the option's price-feed consumer
const twap = await client.ranger.getTWAP(rangerAddress);
```

## Lifecycle

```typescript
// After expiry, the buyer claims the settlement payout
await client.ranger.payout(rangerAddress);

// Either party can close before expiry (mutual cancellation paths)
await client.ranger.close(rangerAddress);

// Split the position by collateral amount (returns child option address)
await client.ranger.split(rangerAddress, splitCollateralAmount);

// Transfer the buyer or seller side
await client.ranger.transfer(rangerAddress, /* isBuyer */ true, recipientAddress);

// Reclaim collateral after settlement
// Note: ownedOption is the option being reclaimed FROM, not a transfer destination.
// The reclaimed collateral goes to the caller (the signer).
// The contract keys getReclaimFee on ownedOption — the SDK forwards it as msg.value.
await client.ranger.reclaimCollateral(rangerAddress, ownedOption);

// Return any excess collateral the contract still holds
await client.ranger.returnExcessCollateral(rangerAddress);
```

## Chain support

`client.ranger` is gated on the chain registry. On chains where `chainConfig.implementations.RANGER` is missing or set to the zero address (e.g., Ethereum mainnet today), every method throws `NETWORK_UNSUPPORTED` up front instead of failing deep inside `eth_call` with a cryptic error.

```typescript
const ethClient = new ThetanutsClient({ chainId: 1, provider });
await ethClient.ranger.getInfo('0x...');
// throws { code: 'NETWORK_UNSUPPORTED',
//          message: 'RangerModule requires a chain with RangerOption deployed; chainId 1 has no RANGER implementation.' }
```

## Fees

`split()` and `reclaimCollateral()` are **payable** in r12. The SDK reads the on-chain fee (`getSplitFee()` and `getReclaimFee(ownedOption)` respectively) and forwards it as `msg.value`. Callers don't need to do anything — just have a signer with enough native-token balance to cover the fee.

## Events

Ranger options emit the same events as every other BaseOption-derived contract: `OptionInitialized`, `OptionPayout`, `OptionClosed`, `OptionExpired`, `OptionSettlementFailed`, `OptionSplit`, `ExcessCollateralReturned`, `TransferApproval`. Query them via `client.events.*` — see the [Events guide](/sdk/guides/events).

## See also

* [Modules Overview](/sdk/sdk-reference/modules-overview#clientranger--rangermodule) — full RangerModule API reference
* [Migration Guide](/sdk/resources/migration-guide) — what changed when moving from v0.1.x or v0.2.0
* [Events guide](/sdk/guides/events) — querying historical Ranger activity


# Token Operations

Read balances, check allowances, and approve token spending using the `client.erc20` module.

## Methods

| Method                                    | Description                                       | Signer |
| ----------------------------------------- | ------------------------------------------------- | ------ |
| `getBalance(token, owner?)`               | Get token balance                                 | No     |
| `getAllowance(token, owner, spender)`     | Get spending allowance                            | No     |
| `getDecimals(token)`                      | Get token decimals (cached)                       | No     |
| `getSymbol(token)`                        | Get token symbol                                  | No     |
| `approve(token, spender, amount)`         | Approve token spending                            | Yes    |
| `ensureAllowance(token, spender, amount)` | Approve only if current allowance is insufficient | Yes    |
| `transfer(token, to, amount)`             | Transfer tokens to address                        | Yes    |
| `encodeApprove(token, spender, amount)`   | Encode approval for external wallet               | No     |

## Usage

### Read Balance and Allowance

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const client = new ThetanutsClient({ chainId: 8453, provider });

const usdcAddress = client.chainConfig.tokens.USDC.address;
const userAddress = '0xYourAddress';

// Get token balance
const balance = await client.erc20.getBalance(usdcAddress, userAddress);
console.log(`Balance: ${ethers.formatUnits(balance, 6)} USDC`);

// Get token decimals (cached after first call)
const decimals = await client.erc20.getDecimals(usdcAddress);
console.log(`Decimals: ${decimals}`); // 6

// Check allowance before a fill or RFQ
const spender = client.chainConfig.contracts.optionBook;
const allowance = await client.erc20.getAllowance(usdcAddress, userAddress, spender);
console.log(`Allowance: ${ethers.formatUnits(allowance, 6)} USDC`);
```

### Approve Spending

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(privateKey, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

const usdcAddress = client.chainConfig.tokens.USDC.address;
const spender = client.chainConfig.contracts.optionBook;

// Approve an exact amount
const receipt = await client.erc20.approve(
  usdcAddress,
  spender,
  1000_000000n  // 1000 USDC (6 decimals)
);
console.log('Approval tx:', receipt.hash);
```

### ensureAllowance (Recommended)

`ensureAllowance()` checks the current allowance first and only submits an approval transaction if needed. This is the preferred pattern before `fillOrder()` or `requestForQuotation()`.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(privateKey, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

const usdcAddress = client.chainConfig.tokens.USDC.address;
const spender = client.chainConfig.contracts.optionBook;
const requiredAmount = 10_000000n; // 10 USDC

const result = await client.erc20.ensureAllowance(
  usdcAddress,
  spender,
  requiredAmount
);

if (result.approved) {
  console.log('Approval tx:', result.txHash);
} else {
  console.log('Allowance was already sufficient — no transaction needed');
}

// Now safe to fill the order
const receipt = await client.optionBook.fillOrder(order, requiredAmount);
```

### Transfer Tokens

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(privateKey, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

const receipt = await client.erc20.transfer(
  client.chainConfig.tokens.USDC.address,
  '0xRecipientAddress',
  50_000000n  // 50 USDC
);
console.log('Transfer tx:', receipt.hash);
```

### Encode Approval for External Wallets

Use `encodeApprove()` when you need to submit the approval through viem, wagmi, a Safe multisig, or another external wallet rather than the built-in ethers signer.

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider });

const { to, data } = client.erc20.encodeApprove(
  client.chainConfig.tokens.USDC.address,
  client.chainConfig.contracts.optionBook,
  10_000000n  // 10 USDC
);

// Send with viem/wagmi
const hash = await walletClient.sendTransaction({ to, data });

// Or with ethers.js
const tx = await signer.sendTransaction({ to, data });
```

## Token Addresses (Base Mainnet)

All addresses are available from `client.chainConfig` — no hardcoding needed.

```typescript
const config = client.chainConfig;

config.tokens.USDC.address;   // 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
config.tokens.USDC.decimals;  // 6
config.tokens.WETH.address;   // 0x4200000000000000000000000000000000000006
config.tokens.WETH.decimals;  // 18
config.tokens.cbBTC.address;  // 0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf
config.tokens.cbBTC.decimals; // 8
```

## Decimal Reference

| Token | Decimals | Example                          |
| ----- | -------- | -------------------------------- |
| USDC  | 6        | `1_000000n` = 1 USDC             |
| WETH  | 18       | `1_000000000000000000n` = 1 WETH |
| cbBTC | 8        | `1_00000000n` = 1 cbBTC          |

Use `client.utils` for safe conversions:

```typescript
const usdc = client.utils.toBigInt('100.5', 6);   // 100500000n
const weth = client.utils.toBigInt('1.5', 18);    // 1500000000000000000n
const display = client.utils.fromBigInt(100500000n, 6);  // '100.5'
```

## See Also

* [Position Management](/sdk/guides/position-management) — approve collateral before closing positions
* [Error Handling](/sdk/guides/error-handling) — `INSUFFICIENT_ALLOWANCE`, `INSUFFICIENT_BALANCE` error codes
* [Production Checklist](/sdk/guides/production-checklist) — collateral approval flow best practices


# WebSocket Subscriptions

Subscribe to real-time order book and price updates using the `client.ws` module.

## Methods

| Method                              | Description                             | Signer |
| ----------------------------------- | --------------------------------------- | ------ |
| `connect(config?)`                  | Open the WebSocket connection           | No     |
| `subscribe(options, callback)`      | Subscribe to a topic                    | No     |
| `unsubscribe(id)`                   | Cancel a subscription                   | No     |
| `subscribeOrders(callback)`         | Convenience: subscribe to order updates | No     |
| `subscribePrices(callback, asset?)` | Convenience: subscribe to price updates | No     |
| `onStateChange(callback)`           | Listen for connection state changes     | No     |
| `getState()`                        | Get current connection state            | No     |
| `disconnect()`                      | Close the connection                    | No     |

## Usage

### Monitor Orders and Prices

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const client = new ThetanutsClient({ chainId: 8453, provider });

// 1. Open the connection
await client.ws.connect();

// 2. Subscribe to order book updates
const unsubOrders = client.ws.subscribeOrders((update) => {
  console.log(`Order ${update.event}:`, update);
});

// 3. Subscribe to ETH price updates
const unsubPrices = client.ws.subscribePrices((update) => {
  console.log(`ETH price: $${update.price}`);
}, 'ETH');

// 4. Monitor connection state
const unsubState = client.ws.onStateChange((state) => {
  console.log(`WebSocket state: ${state}`);
  // state: 'connecting' | 'connected' | 'disconnecting' | 'disconnected'
});

// 5. Clean up when done
unsubOrders();
unsubPrices();
unsubState();
client.ws.disconnect();
```

### Generic subscribe()

Use `subscribe()` directly when you need more control over the subscription topic.

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const client = new ThetanutsClient({ chainId: 8453, provider });

await client.ws.connect();

const subscriptionId = client.ws.subscribe({ type: 'orders' }, (update) => {
  console.log('Order update:', update);
});

// Later, cancel using the returned subscription ID
client.ws.unsubscribe(subscriptionId);
```

### Reconnection Behavior

The WebSocket module auto-reconnects by default (up to 10 attempts). You can observe reconnections via `onStateChange`:

```typescript
client.ws.onStateChange((state) => {
  if (state === 'disconnected') {
    console.log('Connection lost — SDK will auto-reconnect...');
  }
  if (state === 'connected') {
    console.log('Reconnected successfully');
  }
});
```

To manually reconnect after complete disconnection:

```typescript
await client.ws.connect();
```

### Checking Connection State

```typescript
const state = client.ws.getState();
// 'connecting' | 'connected' | 'disconnecting' | 'disconnected'

if (state !== 'connected') {
  await client.ws.connect();
}
```

## Configuration

Pass a custom WebSocket URL at client initialization if you need to override the default endpoint:

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  wsUrl: 'wss://your-custom-ws-endpoint',  // Optional override
});
```

## See Also

* [Events](/sdk/guides/events) — query historical blockchain events when real-time data is not needed
* [Production Checklist](/sdk/guides/production-checklist) — WebSocket reconnection configuration
* [Error Handling](/sdk/guides/error-handling) — `WEBSOCKET_ERROR` error code


# Blockchain Events

Query historical blockchain events from the Thetanuts protocol using the `client.events` module.

## Methods

| Method                                                | Description                                       | Signer |
| ----------------------------------------------------- | ------------------------------------------------- | ------ |
| `getOrderFillEvents(filters?)`                        | OptionBook order fill events                      | No     |
| `getOrderCancelledEvents(filters?)`                   | OptionBook order cancellation events              | No     |
| `getOptionCreatedEvents(filters?)`                    | New option contract creation events               | No     |
| `getQuotationRequestedEvents(filters?)`               | RFQ quotation request events                      | No     |
| `getOfferMadeEvents(filters?)`                        | RFQ encrypted offer submission events             | No     |
| `getOfferRevealedEvents(filters?)`                    | RFQ offer reveal events                           | No     |
| `getQuotationSettledEvents(filters?)`                 | RFQ quotation settlement events                   | No     |
| `getPositionClosedEvents(option, filters?)`           | Position close events for a specific option       | No     |
| `getExcessCollateralReturnedEvents(option, filters?)` | Excess collateral returned to seller (per option) | No     |
| `getOptionSplitEvents(option, filters?)`              | Position split events (per option)                | No     |
| `getTransferApprovalEvents(option, filters?)`         | Transfer-approval events (per option)             | No     |
| `getOptionSettlementFailedEvents(option, filters?)`   | Settlement failures (per option)                  | No     |

> **v0.2.1 rename:** `getCollateralReturnedEvents` was renamed to `getExcessCollateralReturnedEvents` and the event field shape changed to match the r12 contract (`{ seller, collateralToken, collateralReturned }`). See the [Migration Guide](/sdk/resources/migration-guide) for the full diff.

> All event queries auto-chunk block ranges into 10,000-block segments — no manual splitting needed.
>
> When `fromBlock` is omitted, the query searches backward from the latest block (most recent events first).

## Usage

### Filter Common Parameters

Most event methods accept an optional `filters` object:

```typescript
interface EventFilters {
  fromBlock?: number | bigint;  // Start block (default: searches backward from latest)
  toBlock?: number | bigint;    // End block
  quotationId?: bigint;         // Filter by RFQ ID (where applicable)
}
```

### Order Fill Events (OptionBook)

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const client = new ThetanutsClient({ chainId: 8453, provider });

const currentBlock = await provider.getBlockNumber();

const fills = await client.events.getOrderFillEvents({
  fromBlock: currentBlock - 5000,
});

for (const fill of fills) {
  console.log('Fill event:', fill);
}
```

### Order Cancellation Events

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider });

const cancellations = await client.events.getOrderCancelledEvents({
  fromBlock: currentBlock - 5000,
});
```

### Option Created Events (RFQ Settlement)

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider });

const created = await client.events.getOptionCreatedEvents({
  fromBlock: currentBlock - 10000,
});

for (const event of created) {
  console.log('New option contract:', event);
}
```

### RFQ Lifecycle Events

Track the full RFQ lifecycle — from request through offer submission, reveal, and final settlement:

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider });

const quotationId = 784n;

// RFQ was created
const requests = await client.events.getQuotationRequestedEvents({
  fromBlock: currentBlock - 50000,
});

// MM submitted encrypted offer
const offers = await client.events.getOfferMadeEvents({ quotationId });

// MM revealed their offer
const reveals = await client.events.getOfferRevealedEvents({ quotationId });

// Quotation was settled
const settlements = await client.events.getQuotationSettledEvents({ quotationId });
```

### Full RFQ History in One Call

`getRfqHistory(quotationId, filters?)` is a convenience that batches the four RFQ-lifecycle event queries (`requested`, `offerMade`, `offerRevealed`, `settled`) for a single quotation and returns them sorted in chronological order. Skips the bookkeeping you'd do manually:

```typescript
const history = await client.events.getRfqHistory(quotationId);
console.log(history.requested);   // single QuotationRequested event
console.log(history.offers);      // OfferMade events
console.log(history.reveals);     // OfferRevealed events
console.log(history.settlement);  // QuotationSettled event (or null if not settled)
```

### queryEvents — generic catch-all

If you need to scan the OptionBook or OptionFactory ABIs for an arbitrary event signature not covered by a typed helper, fall through to `queryEvents`:

```typescript
const events = await client.events.queryEvents({
  contract: 'optionBook',                    // or 'optionFactory'
  eventName: 'SomeUnusualEvent',
  fromBlock: currentBlock - 1000,
});
```

Most users should reach for the typed helpers above. `queryEvents` is the escape hatch for protocol introspection or custom indexers.

### Using OfferMade Events for Early Settlement

The `getOfferMadeEvents()` result includes the encrypted offer data needed for `settleQuotationEarly()`:

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(privateKey, provider);
const client = new ThetanutsClient({ chainId: 8453, provider, signer });

const quotationId = 784n;

// Find the MM's offer
const offerEvents = await client.events.getOfferMadeEvents({
  quotationId,
  fromBlock: currentBlock - 1000,
});

const offer = offerEvents[0];

// Decrypt and accept early
const keyPair = await client.rfqKeys.loadKeyPair();
const decrypted = await client.rfqKeys.decryptOffer(
  offer.signedOfferForRequester,
  offer.signingKey
);

console.log('Offer amount:', ethers.formatUnits(decrypted.offerAmount, 6), 'USDC');

await client.optionFactory.settleQuotationEarly(
  quotationId,
  decrypted.offerAmount,
  decrypted.nonce,
  offer.offeror
);
```

### Position Closed Events

Query close events for a specific option contract:

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider });

const optionAddress = '0x20D51d70A51Aa529eb9460a49aAC94910A1bc267';

const closedEvents = await client.events.getPositionClosedEvents(
  optionAddress,
  { fromBlock: currentBlock - 10000 }
);

for (const event of closedEvents) {
  console.log('Position closed:', event);
}
```

## See Also

* [WebSocket](/sdk/guides/websocket) — real-time updates instead of historical queries
* [Position Management](/sdk/guides/position-management) — position state queries via contract calls
* [Error Handling](/sdk/guides/error-handling) — `HTTP_ERROR`, `CONTRACT_REVERT` error codes


# Error Handling

All SDK methods throw `ThetanutsError` or a typed subclass. Use the `code` property or `instanceof` to branch on specific errors.

## Error Codes

| Code                     | Description                  | Typical Cause                        |
| ------------------------ | ---------------------------- | ------------------------------------ |
| `ORDER_EXPIRED`          | Order has expired            | Order's expiry timestamp has passed  |
| `SLIPPAGE_EXCEEDED`      | Price moved beyond tolerance | Market moved during transaction      |
| `INSUFFICIENT_ALLOWANCE` | Token approval needed        | Haven't approved tokens for contract |
| `INSUFFICIENT_BALANCE`   | Not enough tokens            | Wallet balance too low               |
| `NETWORK_UNSUPPORTED`    | Chain not supported          | Using unsupported chain ID           |
| `HTTP_ERROR`             | API request failed           | Network issues, API downtime         |
| `BAD_REQUEST`            | Invalid API request          | Malformed parameters                 |
| `NOT_FOUND`              | Resource not found           | Invalid order ID or address          |
| `RATE_LIMIT`             | Rate limit exceeded          | Too many API requests                |
| `CONTRACT_REVERT`        | Contract call failed         | Transaction reverted on-chain        |
| `INVALID_PARAMS`         | Invalid parameters           | Wrong types, missing required fields |
| `INVALID_ORDER`          | Order validation failed      | Malformed order structure            |
| `ORDER_NOT_FOUND`        | Order doesn't exist          | Order ID not in orderbook            |
| `SIZE_EXCEEDED`          | Fill size too large          | Requested more than available        |
| `SIGNER_REQUIRED`        | Signer needed                | Write operation without signer       |
| `WEBSOCKET_ERROR`        | WebSocket error              | Connection issues                    |
| `KEY_NOT_FOUND`          | RFQ key missing              | No ECDH keypair generated            |
| `INVALID_KEY`            | Invalid key format           | Corrupted or wrong key format        |
| `ENCRYPTION_FAILED`      | Encryption failed            | ECDH encryption error                |
| `DECRYPTION_FAILED`      | Decryption failed            | Wrong key or corrupted data          |
| `UNKNOWN`                | Unknown error                | Unexpected error                     |

## Error Classes

### Base Class

```typescript
class ThetanutsError extends Error {
  readonly code: ThetanutsErrorCode;
  readonly cause?: unknown;
  readonly meta?: Record<string, unknown>;
}
```

### API Errors

```typescript
class APIError extends ThetanutsError {
  readonly status: number;  // HTTP status code
}

class BadRequestError extends APIError {}  // HTTP 400
class NotFoundError extends APIError {}    // HTTP 404
class RateLimitError extends APIError {}   // HTTP 429
```

### Contract Errors

```typescript
class ContractRevertError extends ThetanutsError {}
class InsufficientAllowanceError extends ThetanutsError {}
class InsufficientBalanceError extends ThetanutsError {}
class OrderExpiredError extends ThetanutsError {}
class SlippageExceededError extends ThetanutsError {}
```

### Client Errors

```typescript
class SignerRequiredError extends ThetanutsError {}
class InvalidParamsError extends ThetanutsError {}
class NetworkUnsupportedError extends ThetanutsError {}
class WebSocketError extends ThetanutsError {}
```

### RFQ Key Errors

```typescript
class KeyNotFoundError extends ThetanutsError {}   // KEY_NOT_FOUND
class InvalidKeyError extends ThetanutsError {}    // INVALID_KEY
class EncryptionError extends ThetanutsError {}    // ENCRYPTION_FAILED
class DecryptionError extends ThetanutsError {}    // DECRYPTION_FAILED
```

## Handling Errors with `isThetanutsError`

```typescript
import {
  isThetanutsError,
  ThetanutsClient,
} from '@thetanuts-finance/thetanuts-client';

try {
  await client.optionBook.fillOrder(orderWithSig);
} catch (error) {
  if (isThetanutsError(error)) {
    switch (error.code) {
      case 'ORDER_EXPIRED':
        console.log('Order has expired — fetch fresh orders');
        break;
      case 'INSUFFICIENT_ALLOWANCE':
        console.log('Approve tokens before filling');
        break;
      case 'SIGNER_REQUIRED':
        console.log('Initialize client with a signer');
        break;
      default:
        console.log(`SDK error [${error.code}]: ${error.message}`);
    }
  }
}
```

## Handling Errors with `instanceof`

Use typed error classes for branch-specific recovery logic:

```typescript
import {
  ThetanutsError,
  ContractRevertError,
  InsufficientAllowanceError,
  OrderExpiredError,
} from '@thetanuts-finance/thetanuts-client';

try {
  await client.optionBook.fillOrder(order, 10_000000n);
} catch (error) {
  if (error instanceof OrderExpiredError) {
    console.log('Order expired — fetching fresh orders...');
    const freshOrders = await client.api.fetchOrders();
    // retry with a fresh order

  } else if (error instanceof InsufficientAllowanceError) {
    console.log('Approving tokens...');
    await client.erc20.ensureAllowance(
      client.chainConfig.tokens.USDC.address,
      client.chainConfig.contracts.optionBook,
      10_000000n
    );
    // retry

  } else if (error instanceof ContractRevertError) {
    console.log('Contract reverted:', error.message);
    console.log('Cause:', error.cause);

  } else if (error instanceof ThetanutsError) {
    console.log(`SDK error [${error.code}]: ${error.message}`);
    console.log('Meta:', error.meta);  // Additional context
  }
}
```

## Retry Pattern for Transient Errors

Use exponential backoff for rate limiting and transient network errors:

```typescript
import { RateLimitError } from '@thetanuts-finance/thetanuts-client';

async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (error instanceof RateLimitError) {
        const delay = Math.pow(2, attempt) * 1000; // exponential backoff
        console.log(`Rate limited, retrying in ${delay}ms...`);
        await new Promise((r) => setTimeout(r, delay));
        continue;
      }
      throw error; // non-retryable — rethrow immediately
    }
  }
  throw new Error('Max retries exceeded');
}

const orders = await withRetry(() => client.api.fetchOrders());
```

## Contract Revert Reasons

### OptionFactory

| Revert Reason             | Cause                         | Solution                                                    |
| ------------------------- | ----------------------------- | ----------------------------------------------------------- |
| `InvalidImplementation`   | Unknown option implementation | Use valid implementation from `chainConfig.implementations` |
| `InvalidCollateral`       | Unsupported collateral token  | Use USDC, WETH, or cbBTC                                    |
| `InvalidExpiry`           | Expiry in past or too far     | Set expiry between now and a reasonable future date         |
| `InvalidStrikes`          | Strike price invalid          | Ensure strikes are positive and properly scaled             |
| `OfferPeriodEnded`        | Offer period has closed       | Cannot make offer after `offerEndTimestamp`                 |
| `OfferPeriodNotEnded`     | Offer period still open       | Wait for period to end before revealing                     |
| `RevealPeriodEnded`       | Reveal window closed          | Reveal within the reveal window                             |
| `NoOffersToReveal`        | No offers submitted           | At least one MM offer required to settle                    |
| `QuotationAlreadySettled` | Already settled               | Cannot settle twice                                         |
| `NotRequester`            | Wrong caller                  | Only the requester can cancel their RFQ                     |

### OptionBook

| Revert Reason      | Cause               | Solution                |
| ------------------ | ------------------- | ----------------------- |
| `OrderExpired`     | Order has expired   | Fetch fresh orders      |
| `OrderCancelled`   | Order was cancelled | Use a different order   |
| `InsufficientSize` | Not enough to fill  | Reduce fill amount      |
| `InvalidOrder`     | Malformed order     | Re-fetch order from API |

### Option Contract

| Revert Reason      | Cause                  | Solution                                  |
| ------------------ | ---------------------- | ----------------------------------------- |
| `NotExpired`       | Option not expired yet | Wait for expiry before calling `payout()` |
| `AlreadySettled`   | Already settled        | Cannot payout twice                       |
| `NotBuyer`         | Caller is not buyer    | Only buyer can execute payout             |
| `NotBuyerOrSeller` | Wrong caller           | Only buyer/seller can close               |

### ERC20

| Revert Reason                            | Cause                | Solution                                      |
| ---------------------------------------- | -------------------- | --------------------------------------------- |
| `ERC20: insufficient allowance`          | Not approved         | Call `approve()` or `ensureAllowance()` first |
| `ERC20: transfer amount exceeds balance` | Insufficient balance | Check balance before transacting              |

## Common Issues

### INSUFFICIENT\_ALLOWANCE

Always call `ensureAllowance()` before `fillOrder()` or `requestForQuotation()`. The SDK does not auto-approve.

```typescript
await client.erc20.ensureAllowance(
  client.chainConfig.tokens.USDC.address,
  client.chainConfig.contracts.optionBook,
  requiredAmount
);
```

### ORDER\_EXPIRED

Check expiry before filling to avoid wasted gas:

```typescript
const now = Math.floor(Date.now() / 1000);
const validOrders = orders.filter(o => Number(o.order.expiry) > now + 60); // 60s buffer
```

### SIGNER\_REQUIRED

Initialize the client with a signer for any write operation:

```typescript
const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  signer: wallet,  // required for fillOrder, approve, close, payout, etc.
});
```

Alternatively, use the `encode*` methods (e.g., `encodeFillOrder()`) to construct transaction data for an external wallet.

### KEY\_NOT\_FOUND

Generate or restore an ECDH keypair before creating RFQs:

```typescript
const keyPair = await client.rfqKeys.getOrCreateKeyPair();
// Keys are automatically persisted to disk in Node.js.
// Browser apps must configure keyStorageProvider explicitly.
```

### DECRYPTION\_FAILED

Verify you are using the same keypair that was used when the RFQ was created:

```typescript
const currentKeyPair = await client.rfqKeys.getOrCreateKeyPair();
const rfq = await client.api.getRFQFromRfq(quotationId);

if (currentKeyPair.compressedPublicKey !== rfq.requesterPublicKey) {
  console.error('Key mismatch — import the original private key via importFromPrivateKey()');
}
```

## Debugging

### Enable Debug Logging

```typescript
import { consoleLogger, ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  logger: consoleLogger,
});
```

### Inspect Error Details

```typescript
import { ThetanutsError } from '@thetanuts-finance/thetanuts-client';

try {
  await client.optionBook.fillOrder(order);
} catch (error) {
  if (error instanceof ThetanutsError) {
    console.log('Code:', error.code);
    console.log('Message:', error.message);
    console.log('Cause:', error.cause);  // Original underlying error
    console.log('Meta:', error.meta);    // Additional SDK context
  }
}
```

## See Also

* [Token Operations](/sdk/guides/token-operations) — `ensureAllowance` before fills
* [Production Checklist](/sdk/guides/production-checklist) — error logging in production
* [WebSocket](/sdk/guides/websocket) — `WEBSOCKET_ERROR` reconnection handling


# Production Checklist

Items to verify before deploying an application that uses the Thetanuts SDK.

***

## RPC Provider

Do not use the public `https://mainnet.base.org` endpoint in production. Public endpoints have strict rate limits and may be unreliable under sustained load.

```typescript
// Not recommended for production
const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');

// Recommended: use a dedicated RPC provider
const provider = new ethers.JsonRpcProvider(
  'https://base-mainnet.g.alchemy.com/v2/YOUR_API_KEY'
);
// Alternatives:
// https://base-mainnet.infura.io/v3/YOUR_PROJECT_ID
// https://base.quiknode.pro/YOUR_ENDPOINT
```

***

## Referrer Configuration

Set the `referrer` address in the client config to earn fee-sharing revenue on every order fill. If not set, fees go to the zero address.

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  signer,
  referrer: '0xYourReferrerAddress',
});
```

The referrer can be overridden per call if needed:

```typescript
await client.optionBook.fillOrder(order, amount, '0xOverrideReferrer');
```

***

## Error Logging

Pass a custom `logger` to capture SDK errors in your monitoring system (Sentry, Datadog, etc.):

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  logger: {
    debug: (msg, meta) => myLogger.debug(msg, meta),
    info:  (msg, meta) => myLogger.info(msg, meta),
    warn:  (msg, meta) => myLogger.warn(msg, meta),
    error: (msg, meta) => myLogger.error(msg, meta),
  },
});
```

During development, use the built-in `consoleLogger`:

```typescript
import { consoleLogger } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider, logger: consoleLogger });
```

***

## Gas Buffer

The SDK adds a 20% gas buffer automatically for Account Abstraction wallets (Coinbase Smart Wallet, Safe). This buffer also applies for standard EOA wallets as a safety margin. No action required, but factor this into gas cost estimates.

***

## Collateral Approval Flow

The SDK does not auto-approve token spending. Always call `ensureAllowance()` before `fillOrder()`. Failure to do so results in an `INSUFFICIENT_ALLOWANCE` error.

```typescript
// Always approve before filling
await client.erc20.ensureAllowance(
  client.chainConfig.tokens.USDC.address,
  client.chainConfig.contracts.optionBook,
  requiredAmount  // from previewFillOrder()
);

const receipt = await client.optionBook.fillOrder(order, requiredAmount);
```

For SELL (short) positions in the RFQ flow, approve the OptionFactory contract before creating the RFQ:

```typescript
const collateralNeeded = BigInt(Math.round(strike * numContracts * 1e6)); // PUT: strike × contracts
await client.erc20.ensureAllowance(
  client.chainConfig.tokens.USDC.address,
  client.chainConfig.contracts.optionFactory,
  collateralNeeded
);
```

***

## WebSocket Reconnection

The WebSocket module auto-reconnects by default (up to 10 attempts). For high-availability applications, monitor the connection state and configure reconnection settings:

```typescript
client.ws.onStateChange((state) => {
  if (state === 'disconnected') {
    console.warn('WebSocket disconnected — SDK will auto-reconnect');
    // Alert your monitoring system if reconnection is critical
  }
});

await client.ws.connect({
  maxReconnectAttempts: 20,   // override default of 10
  reconnectInterval: 3000,    // ms between attempts
});
```

***

## Order Expiry Checks

Always check `order.expiry` before filling. The SDK throws `OrderExpiredError` when you try to fill an expired order, but checking upfront avoids unnecessary gas estimation calls.

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const orders = await client.api.fetchOrders();
const now = Math.floor(Date.now() / 1000);

// Filter with a 60-second buffer to account for block time
const validOrders = orders.filter(
  (o) => Number(o.order.expiry) > now + 60
);

if (validOrders.length === 0) {
  throw new Error('No active orders available');
}

const order = validOrders[0];
const preview = client.optionBook.previewFillOrder(order, 10_000000n);
```

***

## RFQ Key Backup

If your application creates RFQs, back up the ECDH private key. Without it, you cannot decrypt market maker offers or perform early settlement.

```typescript
// Export key for backup storage
const privateKey = await client.rfqKeys.exportPrivateKey();
// Store securely — losing this means you can't decrypt offers!

// Restore from backup
await client.rfqKeys.importFromPrivateKey(backupPrivateKey, true);
```

Node.js keys are stored in `.thetanuts-keys/` with secure permissions. Browser apps must configure an explicit encrypted/passphrase-backed `keyStorageProvider`; do not rely on plaintext `localStorage` for production RFQ private keys.

***

## Compatibility Requirements

| Requirement | Minimum Version |
| ----------- | --------------- |
| Node.js     | >= 18           |
| ethers.js   | v6              |
| TypeScript  | >= 5.0          |

## See Also

* [Error Handling](/sdk/guides/error-handling) — error codes and retry patterns
* [Token Operations](/sdk/guides/token-operations) — `ensureAllowance` details
* [WebSocket](/sdk/guides/websocket) — connection state and reconnection


# Client

The `ThetanutsClient` is the single entry point to the Thetanuts SDK — it initializes all modules, manages provider/signer connections, and exposes chain configuration.

## Constructor

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({
  chainId: 8453,              // Required: Chain ID (Base = 8453)
  provider: Provider,         // Required: ethers.js provider
  signer?: Signer,            // Optional: For write operations
  referrer?: string,          // Optional: Referrer address for fees
  apiBaseUrl?: string,        // Optional: Override API URL
  indexerApiUrl?: string,     // Optional: Override indexer URL
  pricingApiUrl?: string,     // Optional: Override pricing URL
  wsUrl?: string,             // Optional: Override WebSocket URL
  stateApiUrl?: string,       // Optional: Override RFQ state indexer URL
  env?: 'dev' | 'prod',       // Optional: Environment (default: prod)
  logger?: ThetanutsLogger,   // Optional: Custom logger
  keyStorageProvider?: KeyStorageProvider,  // Optional: Custom RFQ key storage
  rfqKeyPrefix?: string,      // Optional: Custom RFQ key prefix
});
```

## Properties

| Property        | Type                  | Description                                   |
| --------------- | --------------------- | --------------------------------------------- |
| `chainId`       | `number`              | Active chain ID (e.g. `8453`)                 |
| `chainConfig`   | `ChainConfig`         | Chain configuration with addresses and tokens |
| `provider`      | `Provider`            | ethers.js provider instance                   |
| `signer`        | `Signer \| undefined` | Signer for transactions                       |
| `referrer`      | `string \| undefined` | Default referrer address                      |
| `apiBaseUrl`    | `string`              | Orders API endpoint URL                       |
| `indexerApiUrl` | `string`              | Indexer API endpoint URL                      |
| `pricingApiUrl` | `string`              | Pricing API endpoint URL                      |
| `stateApiUrl`   | `string`              | RFQ state indexer URL                         |

## Modules Access

All SDK modules are accessed as properties on the client instance:

```typescript
client.erc20          // Token operations (approvals, balances, transfers)
client.optionBook     // Order book operations (fill, cancel, fees)
client.api            // API interactions (orders, positions, stats)
client.optionFactory  // RFQ lifecycle management
client.option         // Position management
client.ranger         // RangerOption zone-bound positions
client.events         // Blockchain events
client.ws             // WebSocket subscriptions
client.mmPricing      // Market maker pricing
client.rfqKeys        // ECDH key management and offer encryption
client.utils          // Utility functions (decimals, payoffs)
client.loan           // Non-liquidatable lending
client.collar         // Collar loan pricing and write helpers
client.wheelVault     // Ethereum wheel vaults
client.strategyVault  // Base strategy vaults
```

## Chain Config Access

```typescript
const config = client.chainConfig;

// Tokens
config.tokens.USDC.address   // '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
config.tokens.USDC.decimals  // 6
config.tokens.WETH.address   // '0x4200000000000000000000000000000000000006'
config.tokens.WETH.decimals  // 18
config.tokens.cbBTC.address  // '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf'
config.tokens.cbBTC.decimals // 8

// Option implementations — cash-settled
config.implementations.PUT
config.implementations.INVERSE_CALL
config.implementations.LINEAR_CALL
config.implementations.PUT_SPREAD
config.implementations.CALL_SPREAD
config.implementations.INVERSE_CALL_SPREAD
config.implementations.PUT_FLY
config.implementations.CALL_FLY
config.implementations.PUT_CONDOR
config.implementations.CALL_CONDOR
config.implementations.IRON_CONDOR
config.implementations.RANGER       // r12: zone-bound (4-strike) RangerOption
config.implementations.CALL_LOAN    // r12: physically-settled call loan handler

// Option implementations — physically settled (vanilla deployed in r12)
config.implementations.PHYSICAL_CALL
config.implementations.PHYSICAL_PUT

// Option implementations — physically settled multi-leg (NOT YET DEPLOYED in r12;
// addresses below are 0x000…000 placeholders. RFQ flows that try to route through
// these throw INVALID_PARAMS at the SDK level — see optionFactory.ts:2241).
config.implementations.PHYSICAL_CALL_SPREAD
config.implementations.PHYSICAL_PUT_SPREAD
config.implementations.PHYSICAL_CALL_FLY
config.implementations.PHYSICAL_PUT_FLY
config.implementations.PHYSICAL_CALL_CONDOR
config.implementations.PHYSICAL_PUT_CONDOR
config.implementations.PHYSICAL_IRON_CONDOR

// Price feeds
config.priceFeeds.ETH  // Chainlink ETH/USD feed
config.priceFeeds.BTC  // Chainlink BTC/USD feed

// Contracts
config.contracts.optionFactory
config.contracts.optionBook

// r12: HistoricalPriceConsumerV3_TWAP (Chainlink TWAP consumer used at settlement).
// null on chains where the consumer is not deployed.
config.twapConsumer
```

## Helper Methods

### getSignerAddress()

Get the address of the current signer. Throws `SIGNER_REQUIRED` if no signer is configured.

```typescript
const address = await client.getSignerAddress();
// Returns: '0x1234...'
```

### requireSigner()

Get the signer instance, throwing `SIGNER_REQUIRED` if unavailable.

```typescript
const signer = client.requireSigner();
```

## Initialization Patterns

### Read-only (no signer)

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');

const client = new ThetanutsClient({
  chainId: 8453,
  provider,
});

// Read-only operations work without a signer
const orders = await client.api.fetchOrders();
const balance = await client.erc20.getBalance(tokenAddress, userAddress);
```

### With signer (for transactions)

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.JsonRpcProvider('https://mainnet.base.org');
const signer = new ethers.Wallet(process.env.PRIVATE_KEY!, provider);

const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  signer,
  referrer: '0xYourReferrerAddress',
});

await client.erc20.approve(tokenAddress, spenderAddress, amount);
await client.optionBook.fillOrder(order, 10_000000n);
```

### Browser with MetaMask

```typescript
import { ethers } from 'ethers';
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();

const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  signer,
});
```

### Custom logger

```typescript
import { ThetanutsClient, consoleLogger } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  logger: consoleLogger,
});
```

### URL overrides

```typescript
const client = new ThetanutsClient({
  chainId: 8453,
  provider,
  apiBaseUrl: 'https://my-custom-api.example.com',
  indexerApiUrl: 'https://my-indexer.example.com/api/v1',
  pricingApiUrl: 'https://my-pricing.example.com',
  stateApiUrl: 'https://my-state-api.example.com',
  wsUrl: 'wss://my-websocket.example.com',
});
```

## Error Handling

The client throws `ThetanutsError` for typed error conditions:

```typescript
import { isThetanutsError } from '@thetanuts-finance/thetanuts-client';

try {
  const address = await client.getSignerAddress();
} catch (error) {
  if (isThetanutsError(error) && error.code === 'SIGNER_REQUIRED') {
    console.log('Please connect a wallet');
  }
}
```

## Architecture

```
ThetanutsClient
├── chainConfig     ← Addresses, tokens, price feeds
├── provider        ← ethers.js provider
├── signer          ← Optional signer for write ops
│
├── erc20           ← ERC20Module
├── optionBook      ← OptionBookModule
├── api             ← APIModule
├── optionFactory   ← OptionFactoryModule
├── option          ← OptionModule
├── ranger          ← RangerModule
├── events          ← EventsModule
├── ws              ← WebSocketModule
├── mmPricing       ← MMPricingModule
├── rfqKeys         ← RFQKeyManagerModule
├── utils           ← UtilsModule
├── loan            ← LoanModule
├── collar          ← CollarModule
├── wheelVault      ← WheelVaultModule
└── strategyVault   ← StrategyVaultModule
```

***

## See Also

* [Modules Overview](/sdk/sdk-reference/modules-overview) — All 15 modules at a glance
* [Chain Config](/sdk/sdk-reference/chain-config) — Full chain configuration reference
* [../getting-started/configuration.md](/sdk/getting-started/configuration) — Getting started with configuration


# Modules Overview

All 15 SDK modules are accessed as properties on the `ThetanutsClient` instance — no separate instantiation needed.

## Module Table

| Module        | Client Property        | Purpose                                                 | Requires Signer |
| ------------- | ---------------------- | ------------------------------------------------------- | --------------- |
| ERC20         | `client.erc20`         | Token approvals, balances, transfers                    | Write ops only  |
| OptionBook    | `client.optionBook`    | Fill/cancel orders, fee claiming                        | Write ops only  |
| API           | `client.api`           | Fetch orders, positions, stats                          | No              |
| OptionFactory | `client.optionFactory` | RFQ lifecycle management                                | Write ops only  |
| Option        | `client.option`        | Position management and payouts                         | Write ops only  |
| Ranger        | `client.ranger`        | RangerOption (zone-bound, 4-strike) position management | Write ops only  |
| Events        | `client.events`        | Query blockchain events                                 | No              |
| WebSocket     | `client.ws`            | Real-time subscriptions                                 | No              |
| MM Pricing    | `client.mmPricing`     | Market maker pricing and Greeks                         | No              |
| RFQ Keys      | `client.rfqKeys`       | ECDH key management and offer encryption                | No              |
| Utils         | `client.utils`         | Decimal conversions, payoff calculations                | No              |
| Loan          | `client.loan`          | Non-liquidatable lending                                | Write ops only  |
| Collar        | `client.collar`        | Zero-interest collar loans and collar pricing helpers   | Write ops only  |
| WheelVault    | `client.wheelVault`    | WheelVault (Ethereum mainnet) interactions              | Write ops only  |
| StrategyVault | `client.strategyVault` | StrategyVault (Base) interactions                       | Write ops only  |

## Module Descriptions

### `client.erc20` — ERC20Module

Handles all ERC-20 token operations: reading balances and allowances, approving spenders, and transferring tokens. Provides `ensureAllowance()` to conditionally approve only when the existing allowance is insufficient.

Relevant section: [OptionBook overview](/sdk/optionbook/overview)

### `client.optionBook` — OptionBookModule

Fills and cancels orders on the OptionBook contract. Use `previewFillOrder()` for a dry-run before executing. Also handles referrer fee tracking and claiming across all collateral tokens.

Relevant section: [OptionBook overview](/sdk/optionbook/overview)

### `client.api` — APIModule

Fetches data from Thetanuts APIs. Methods are read-only and organized by source: `*FromIndexer` for the book indexer, `*FromRfq` for the RFQ/factory indexer. Covers orders, user positions, trade history, protocol stats, and daily time series.

### `client.optionFactory` — OptionFactoryModule

Manages the full RFQ (Request for Quotation) lifecycle: building parameters, submitting requests, polling quotation state, revealing offers, and settling. Supports vanilla, spread, butterfly, condor, iron condor, and physically settled structures. Also exposes the RFQ-side referral surface (`registerReferral`, `getReferralOwner`, `getReferralFees`, `withdrawFees`) — see the [RFQ Referrals](/sdk/rfq-factory/referrals) guide.

Relevant section: [RFQ overview](/sdk/rfq-factory/overview)

### `client.option` — OptionModule

Queries and manages individual option positions. Reads option info, checks expiry/settlement state, calculates payouts at a given price, and executes close/transfer/split/payout operations.

### `client.ranger` — RangerModule

Manages RangerOption positions — the r12 zone-bound, 4-strike payoff. The buyer earns the maximum payout when the settlement price lands inside the zone (between strikes 2 and 3) and a linearly decaying payout outside. Reads include `getInfo()`, `getZone()`, `getSpreadWidth()`, `getStrikes()`, `getTWAP()`, `calculatePayout(price)`, `simulatePayout(...)`, and `calculateRequiredCollateral(...)`. Writes include `payout()`, `close()`, `split(amount)`, `transfer(isBuyer, target)`, `reclaimCollateral(ownedOption)`, and `returnExcessCollateral()`.

The module throws `NETWORK_UNSUPPORTED` on chains where RangerOption is not deployed (e.g., Ethereum mainnet today) — no silent eth\_call failures.

### `client.events` — EventsModule

Queries on-chain events from the Thetanuts contracts: order fills, cancellations, RFQ requests, MM offers, offer reveals, settlements, and position closes. Block ranges are auto-chunked into 10K-block segments.

### `client.ws` — WebSocketModule

Provides real-time subscriptions over WebSocket for order updates, price feeds, and connection state changes. Includes automatic reconnection.

### `client.mmPricing` — MMPricingModule

Fetches market maker pricing from the pricing API. Returns fee-adjusted bid/ask prices per collateral token with collateral cost included. Includes filter and sort helpers and multi-leg pricing for spreads, butterflies, and condors.

Relevant section: [MM Pricing guide](/sdk/pricing/mm-pricing)

### `client.rfqKeys` — RFQKeyManagerModule

Manages ECDH key pairs used in the sealed-bid auction RFQ flow. Generates, stores, loads, and backs up keys. Provides `encryptOffer()` for market makers and `decryptOffer()` for requesters.

Relevant section: [RFQ overview](/sdk/rfq-factory/overview)

### `client.utils` — UtilsModule

Pure utility functions with no network calls: decimal conversions (`toBigInt`, `fromBigInt`, `strikeToChain`, `strikeFromChain`, `toUsdcDecimals`, `fromUsdcDecimals`), payout calculations, collateral calculations, and payoff diagram data generation.

Relevant section: [Utilities](/sdk/sdk-reference/utilities)

### `client.loan` — LoanModule

Non-liquidatable lending. Borrowers deposit ETH/BTC collateral and receive USDC. At expiry, repay to reclaim collateral or walk away. Includes pricing, cost calculation, lending opportunities, and option exercise.

Relevant section: [Loan overview](/sdk/loan/overview)

### `client.collar` — CollarModule

Zero-interest collar loan helpers. Pricing and strike filtering work today; on-chain write methods are gated until collar-v12 contract addresses are populated.

Relevant section: [Collar loans](/sdk/loan/collar)

### `client.wheelVault` — WheelVaultModule

Reads and interacts with the WheelVault contracts on Ethereum mainnet. The module is chain-gated — methods throw `NETWORK_UNSUPPORTED` when the client is configured for any chain other than `chainId 1`.

Relevant section: [Wheel Vault overview](/sdk/wheel-vault-ethereum/overview)

### `client.strategyVault` — StrategyVaultModule

Reads and interacts with the StrategyVault contracts (fixed-strike and CLVEX strategies) on Base. The module is chain-gated — methods throw `NETWORK_UNSUPPORTED` when the client is configured for any chain other than `chainId 8453`.

Relevant section: [Strategy Vault overview](/sdk/strategy-vault-base/overview)

## Quick Reference

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider, signer });

// ERC20
const balance = await client.erc20.getBalance(tokenAddress, userAddress);
await client.erc20.ensureAllowance(token, spender, amount);

// OptionBook
const preview = client.optionBook.previewFillOrder(order, 10_000000n);
const receipt = await client.optionBook.fillOrder(order, 10_000000n);

// API
const orders = await client.api.fetchOrders();
const positions = await client.api.getUserPositionsFromIndexer(address);

// OptionFactory
const request = client.optionFactory.buildRFQRequest({ /* params */ });
await client.optionFactory.requestForQuotation(request);

// Option
const info = await client.option.getFullOptionInfo(optionAddress);

// Ranger (RangerOption — zone-bound, 4-strike)
const rangerInfo = await client.ranger.getInfo(rangerAddress);
const { zoneLower, zoneUpper } = await client.ranger.getZone(rangerAddress);

// Events
const fills = await client.events.getOrderFillEvents();

// WebSocket
await client.ws.connect();
client.ws.subscribeOrders((update) => console.log(update));

// MM Pricing
const all = await client.mmPricing.getAllPricing('ETH');
const active = client.mmPricing.filterExpired(Object.values(all));

// RFQ Keys
const keyPair = await client.rfqKeys.getOrCreateKeyPair();

// Utils
const onChain = client.utils.strikeToChain(1850);  // 185000000000n
const human = client.utils.fromBigInt(100500000n, 6);  // '100.5'

// Loan
const strikes = await client.loan.getStrikeOptions('ETH');
const calc = client.loan.calculateLoan({ depositAmount: '1.0', underlying: 'ETH', strike: 1600, expiryTimestamp: 1780041600, askPrice: 0.007, underlyingPrice: 2328 });
```

***

## See Also

* [Client](/sdk/sdk-reference/client) — ThetanutsClient constructor and properties
* [Utilities](/sdk/sdk-reference/utilities) — UtilsModule method reference
* [../getting-started/configuration.md](/sdk/getting-started/configuration) — Getting started with configuration


# Utilities

Pure utility functions for decimal conversions, payout calculations, and payoff diagram generation — no network calls required.

Access via `client.utils`.

## Method Reference

| Method                          | Description                                          | Signer |
| ------------------------------- | ---------------------------------------------------- | ------ |
| `toBigInt(value, decimals)`     | Convert human-readable string/number to bigint       | No     |
| `fromBigInt(value, decimals)`   | Convert bigint to human-readable string              | No     |
| `strikeToChain(strike)`         | Convert strike price to on-chain format (8 decimals) | No     |
| `strikeFromChain(value)`        | Convert on-chain strike to human-readable number     | No     |
| `toUsdcDecimals(value)`         | Convert to USDC (6 decimals)                         | No     |
| `fromUsdcDecimals(value)`       | Convert from USDC bigint to string                   | No     |
| `calculatePayout(params)`       | Option payoff calculation                            | No     |
| `calculateCollateral(params)`   | Required collateral for a position                   | No     |
| `generatePayoffDiagram(params)` | Payoff chart data points                             | No     |

## Decimal Conversions

### toBigInt()

Convert a human-readable value to a bigint with the specified decimal precision. Uses string-based parsing to avoid floating-point errors.

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider });

const usdc = client.utils.toBigInt('100.5', 6);   // 100500000n
const weth = client.utils.toBigInt('1.5', 18);    // 1500000000000000000n
const btc  = client.utils.toBigInt('0.001', 8);   // 100000n
```

### fromBigInt()

Convert a bigint back to a human-readable string.

```typescript
const display = client.utils.fromBigInt(100500000n, 6);          // '100.5'
const eth     = client.utils.fromBigInt(1500000000000000000n, 18); // '1.5'
```

## Strike / Price Conversions

Strikes and prices are stored on-chain with 8 decimal places. Use these helpers instead of manual multiplication to avoid floating-point errors.

### strikeToChain()

```typescript
const strikeOnChain = client.utils.strikeToChain(1850.5);
// Returns: 185050000000n  (8 decimals)

const btcStrike = client.utils.strikeToChain(95000);
// Returns: 9500000000000n
```

### strikeFromChain()

```typescript
const strikeNumber = client.utils.strikeFromChain(185050000000n);
// Returns: 1850.5

const price = client.utils.strikeFromChain(9500000000000n);
// Returns: 95000
```

## Convenience Methods

```typescript
// USDC (6 decimals)
client.utils.toUsdcDecimals('100.5');       // 100500000n
client.utils.fromUsdcDecimals(100500000n);  // '100.5'

// Strike / price (8 decimals)  — same as strikeToChain / strikeFromChain
client.utils.toStrikeDecimals(1850);            // 185000000000n
client.utils.fromStrikeDecimals(185000000000n); // '1850'
```

## Payout Calculation

### calculatePayout()

Calculate option payoff at a given settlement price. Pure math — no chain calls.

```typescript
const payout = client.utils.calculatePayout({
  type: 'call',                           // see PayoutType union below
  strikes: [200000000000n],               // Strike(s) in 8 decimals
  settlementPrice: 250000000000000000n,   // Settlement price in 8 decimals
  numContracts: 10000000000000000000n,    // In 18 decimals (SIZE decimals)
});
```

**Supported types and strike orderings.** Strike order must match what the on-chain factory expects — pass strikes exactly as listed:

| `type`          | Strikes | Order                                                       | Invariant                                      |
| --------------- | ------- | ----------------------------------------------------------- | ---------------------------------------------- |
| `'call'`        | 1       | `[strike]`                                                  | —                                              |
| `'put'`         | 1       | `[strike]`                                                  | —                                              |
| `'call_spread'` | 2       | `[lower, upper]` ASCENDING                                  | —                                              |
| `'put_spread'`  | 2       | `[lower, upper]` ASCENDING                                  | —                                              |
| `'call_fly'`    | 3       | `[K1, K2, K3]` ASCENDING                                    | `K2 - K1 === K3 - K2` (equidistant)            |
| `'put_fly'`     | 3       | `[K3, K2, K1]` DESCENDING                                   | `K3 - K2 === K2 - K1` (equidistant)            |
| `'call_condor'` | 4       | `[K1, K2, K3, K4]` ASCENDING                                | `K2 - K1 === K4 - K3` (equal wings)            |
| `'put_condor'`  | 4       | `[K1, K2, K3, K4]` ASCENDING (condors are always ascending) | `K2 - K1 === K4 - K3`                          |
| `'iron_condor'` | 4       | `[putLower, putUpper, callLower, callUpper]`                | `putUpper <= callLower`                        |
| `'ranger'`      | 4       | `[callLower, callUpper, putLower, putUpper]`                | spread widths equal AND `callUpper < putLower` |

**Multi-leg example (iron condor — buyer's payoff):**

```typescript
const payout = client.utils.calculatePayout({
  type: 'iron_condor',
  strikes: [
    180000000000n, // putLower  $1800
    190000000000n, // putUpper  $1900
    210000000000n, // callLower $2100
    220000000000n, // callUpper $2200
  ],
  settlementPrice: 175000000000n, // $1750 (left wing fully ITM)
  numContracts: 1000000000000000000n,
});
// Returns 100000000n (= $100 USDC, capped at max(put-spread, call-spread))
```

**Ranger example (zone-bound):**

```typescript
const payout = client.utils.calculatePayout({
  type: 'ranger',
  strikes: [
    190000000000n, // callLower $1900
    200000000000n, // callUpper $2000
    210000000000n, // putLower  $2100
    220000000000n, // putUpper  $2200
  ],
  settlementPrice: 205000000000n, // $2050 (inside the zone)
  numContracts: 1000000000000000000n,
});
// Returns 100000000n (= $100 USDC, max payout = spread width k)
```

Ranger seller posts `2k` collateral (use `calculateCollateral`) even though the buyer's max payoff is `k` — either ramp can max out independently.

### calculateCollateral()

Calculate the required collateral for a position.

```typescript
const collateral = client.utils.calculateCollateral({
  // params depend on option type; see API Reference for full signature
});
```

### generatePayoffDiagram()

Generate an array of `{ price, payout }` data points for rendering a payoff chart.

```typescript
const diagram = client.utils.generatePayoffDiagram({
  type: 'put_spread',
  strikes: [180000000000n, 200000000000n],  // 8 decimals
  numContracts: 1000000000000000000n,       // 1 contract in 18 decimals
  priceRange: { min: 150000000000n, max: 250000000000n },
  steps: 100,
});

// Returns: Array<{ price: bigint; payout: bigint }>
for (const { price, payout } of diagram) {
  console.log(`$${client.utils.strikeFromChain(price)}: ${payout}`);
}
```

## Decimal Constants

Import the `DECIMALS` constant to avoid hardcoding decimal values:

```typescript
import { DECIMALS } from '@thetanuts-finance/thetanuts-client';

DECIMALS.USDC   // 6
DECIMALS.WETH   // 18
DECIMALS.cbBTC  // 8
DECIMALS.PRICE  // 8  (strikes and settlement prices)
DECIMALS.SIZE   // 18 (numContracts)
```

## Full Conversion Example

```typescript
import { ThetanutsClient, DECIMALS } from '@thetanuts-finance/thetanuts-client';
import { ethers } from 'ethers';

const client = new ThetanutsClient({ chainId: 8453, provider });

// Human-readable inputs
const strikeUsd = 1850;
const numContracts = 1.5;
const collateralUsdc = '100.50';

// Convert to on-chain values
const strikeOnChain   = client.utils.strikeToChain(strikeUsd);        // 185000000000n
const contractsOnChain = client.utils.toBigInt(String(numContracts), DECIMALS.SIZE); // 1500000000000000000n
const usdcOnChain     = client.utils.toUsdcDecimals(collateralUsdc);   // 100500000n

// Convert back for display
console.log('Strike:', client.utils.strikeFromChain(strikeOnChain));           // 1850
console.log('Contracts:', client.utils.fromBigInt(contractsOnChain, DECIMALS.SIZE)); // '1.5'
console.log('USDC:', client.utils.fromUsdcDecimals(usdcOnChain));              // '100.5'
```

***

## See Also

* [Decimal Reference](/sdk/sdk-reference/decimals) — Token decimal table and conversion rules
* [Modules Overview](/sdk/sdk-reference/modules-overview) — All 15 modules at a glance
* [Chain Config](/sdk/sdk-reference/chain-config) — Token addresses and decimals per chain


# Type Exports

All TypeScript types and classes exported from `@thetanuts-finance/thetanuts-client`, grouped by domain.

## Full Import Example

```typescript
import type {
  // Client
  ThetanutsClientConfig,
  ChainConfig,

  // RFQ
  RFQBuilderParams,
  RFQRequest,
  QuotationParameters,
  QuotationTracking,
  RFQUnderlying,
  RFQOptionType,
  RFQCollateralToken,

  // Multi-leg RFQ helpers
  SpreadRFQParams,
  ButterflyRFQParams,
  CondorRFQParams,
  IronCondorRFQParams,
  PhysicalOptionRFQParams,
  PhysicalSpreadRFQParams,
  PhysicalButterflyRFQParams,
  PhysicalCondorRFQParams,
  PhysicalIronCondorRFQParams,

  // Option
  OptionInfo,
  FullOptionInfo,
  PayoutCalculation,

  // MM Pricing
  MMVanillaPricing,
  MMPositionPricing,
  MMSpreadPricing,
  MMCondorPricing,
  MMButterflyPricing,

  // Utils
  PayoutType,
  PayoutParams,

  // API
  OrderWithSignature,
  Position,
  PositionSettlement,
  TradeHistory,
  APIProtocolStats,

  // RFQ Key Manager
  RFQKeyPair,
  EncryptedOffer,
  DecryptedOffer,
  KeyStorageProvider,
} from '@thetanuts-finance/thetanuts-client';

// Storage Providers (concrete classes, not just types)
import {
  LocalStorageProvider,
  MemoryStorageProvider,
} from '@thetanuts-finance/thetanuts-client';
```

## Grouped by Domain

### Client

| Export                  | Kind   | Description                                                |
| ----------------------- | ------ | ---------------------------------------------------------- |
| `ThetanutsClientConfig` | `type` | Constructor options for `ThetanutsClient`                  |
| `ChainConfig`           | `type` | Chain configuration shape (contracts, tokens, feeds, URLs) |

### RFQ

| Export                | Kind   | Description                                                                                     |
| --------------------- | ------ | ----------------------------------------------------------------------------------------------- |
| `RFQBuilderParams`    | `type` | Parameters for `buildRFQParams()`                                                               |
| `RFQRequest`          | `type` | Complete RFQ request ready for submission                                                       |
| `QuotationParameters` | `type` | On-chain quotation parameters                                                                   |
| `QuotationTracking`   | `type` | Tracking fields (referralId, eventCode)                                                         |
| `RFQUnderlying`       | `type` | `'ETH' \| 'BTC' \| 'SOL' \| 'DOGE' \| 'XRP' \| 'BNB' \| 'PAXG' \| 'AVAX'`                       |
| `RFQOptionType`       | `type` | `'CALL' \| 'PUT'`                                                                               |
| `RFQCollateralToken`  | `type` | `'USDC' \| 'WETH' \| 'cbBTC' \| 'aBasWETH' \| 'aBascbBTC' \| 'aBasUSDC' \| 'cbDOGE' \| 'cbXRP'` |

### Multi-leg RFQ Helpers

| Export                        | Kind   | Description                                       |
| ----------------------------- | ------ | ------------------------------------------------- |
| `SpreadRFQParams`             | `type` | Parameters for `buildSpreadRFQ()` (2 strikes)     |
| `ButterflyRFQParams`          | `type` | Parameters for `buildButterflyRFQ()` (3 strikes)  |
| `CondorRFQParams`             | `type` | Parameters for `buildCondorRFQ()` (4 strikes)     |
| `IronCondorRFQParams`         | `type` | Parameters for `buildIronCondorRFQ()` (4 strikes) |
| `PhysicalOptionRFQParams`     | `type` | Parameters for `buildPhysicalOptionRFQ()`         |
| `PhysicalSpreadRFQParams`     | `type` | Parameters for `buildPhysicalSpreadRFQ()`         |
| `PhysicalButterflyRFQParams`  | `type` | Parameters for `buildPhysicalButterflyRFQ()`      |
| `PhysicalCondorRFQParams`     | `type` | Parameters for `buildPhysicalCondorRFQ()`         |
| `PhysicalIronCondorRFQParams` | `type` | Parameters for `buildPhysicalIronCondorRFQ()`     |

### Option

| Export              | Kind   | Description                                                                               |
| ------------------- | ------ | ----------------------------------------------------------------------------------------- |
| `OptionInfo`        | `type` | Basic option info (type, strikes, expiry, collateral, priceFeed, implementation)          |
| `FullOptionInfo`    | `type` | Full option state including buyer, seller, contracts, collateral, expiry/settlement flags |
| `PayoutCalculation` | `type` | Return type of payout calculation methods                                                 |

### MM Pricing

| Export               | Kind   | Description                                                             |
| -------------------- | ------ | ----------------------------------------------------------------------- |
| `MMVanillaPricing`   | `type` | Pricing for a single vanilla option (bid/ask, IV, collateral breakdown) |
| `MMPositionPricing`  | `type` | Position pricing including collateral cost and total price              |
| `MMSpreadPricing`    | `type` | Spread pricing with width, collateral cost, MM bid/ask                  |
| `MMCondorPricing`    | `type` | Condor pricing with four legs and spread collateral cost                |
| `MMButterflyPricing` | `type` | Butterfly pricing with three legs and wing width                        |

### Utils

| Export         | Kind   | Description                                                                                                                                 |
| -------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `PayoutType`   | `type` | `'call' \| 'put' \| 'call_spread' \| 'put_spread' \| 'call_fly' \| 'put_fly' \| 'call_condor' \| 'put_condor' \| 'iron_condor' \| 'ranger'` |
| `PayoutParams` | `type` | Parameters for `calculatePayout()`                                                                                                          |

### API

| Export               | Kind   | Description                                                        |
| -------------------- | ------ | ------------------------------------------------------------------ |
| `OrderWithSignature` | `type` | Order as returned from `client.api.fetchOrders()`                  |
| `Position`           | `type` | User position with full details from the indexer                   |
| `PositionSettlement` | `type` | Settlement details (price, payouts, exercised flag, oracle status) |
| `TradeHistory`       | `type` | Trade history entry                                                |
| `APIProtocolStats`   | `type` | Protocol-wide statistics from the API module                       |

### RFQ Key Manager

| Export               | Kind        | Description                                                |
| -------------------- | ----------- | ---------------------------------------------------------- |
| `RFQKeyPair`         | `type`      | ECDH key pair (privateKey, compressedPublicKey, publicKey) |
| `EncryptedOffer`     | `type`      | Encrypted offer ciphertext and signing key                 |
| `DecryptedOffer`     | `type`      | Decrypted offer (offerAmount, nonce)                       |
| `KeyStorageProvider` | `interface` | Interface for implementing custom key storage backends     |

### Storage Providers (classes)

| Export                  | Kind    | Description                                                  |
| ----------------------- | ------- | ------------------------------------------------------------ |
| `LocalStorageProvider`  | `class` | Browser localStorage storage for explicit opt-in/testing use |
| `MemoryStorageProvider` | `class` | In-memory storage for testing only (keys lost on exit)       |

## Usage in TypeScript Projects

When building integrations, import only the types you need:

```typescript
import type { RFQBuilderParams, RFQCollateralToken } from '@thetanuts-finance/thetanuts-client';

function buildMyRFQ(
  strike: number,
  collateral: RFQCollateralToken,
): RFQBuilderParams {
  return {
    requester: '0x...',
    underlying: 'ETH',
    optionType: 'PUT',
    strikes: strike,
    expiry: Math.floor(Date.now() / 1000) + 86400 * 7,
    numContracts: 1,
    isLong: true,
    offerDeadlineMinutes: 60,
    collateralToken: collateral,
  };
}
```

***

## See Also

* [Modules Overview](/sdk/sdk-reference/modules-overview) — All 15 modules at a glance
* [Client](/sdk/sdk-reference/client) — ThetanutsClient constructor and properties
* [../rfq/overview.md](/sdk/rfq-factory/overview) — RFQ module and workflow


# Chain Config

The chain configuration bundles all on-chain addresses, token metadata, price feeds, and API endpoints for a supported network. Access it via `client.chainConfig`.

Currently supported: **Base Mainnet** (chain ID `8453`) for the full options surface, and **Ethereum Mainnet** (chain ID `1`) for vault-only modules (`client.wheelVault`). Collar pricing/preview helpers are available on Base, but on-chain collar write methods are gated until collar-v12 contract addresses are populated. See [Supported Chains](/sdk/getting-started/supported-chains) for the full breakdown.

## Accessing Chain Config

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider });
const config = client.chainConfig;
```

## Tokens

| Symbol    | Address                                      | Decimals | Description           |
| --------- | -------------------------------------------- | -------- | --------------------- |
| USDC      | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | 6        | USD Coin              |
| WETH      | `0x4200000000000000000000000000000000000006` | 18       | Wrapped Ether         |
| cbBTC     | `0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf` | 8        | Coinbase Wrapped BTC  |
| aBasWETH  | `0xD4a0e0b9149BCee3C920d2E00b5dE09138fd8bb7` | 18       | Aave Base WETH        |
| aBascbBTC | `0xBdb9300b7CDE636d9cD4AFF00f6F009fFBBc8EE6` | 8        | Aave Base cbBTC       |
| aBasUSDC  | `0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB` | 6        | Aave Base USDC        |
| cbDOGE    | `0x73c7A9C372F31c1b1C7f8E5A7D12B8735c817C79` | 8        | Coinbase Wrapped DOGE |
| cbXRP     | `0x7B2Cd9EA5566c345C9cdbcF58f5E211a0dB47444` | 6        | Coinbase Wrapped XRP  |

```typescript
config.tokens.USDC.address;      // '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
config.tokens.USDC.decimals;     // 6
config.tokens.WETH.address;      // '0x4200000000000000000000000000000000000006'
config.tokens.WETH.decimals;     // 18
config.tokens.cbBTC.address;     // '0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf'
config.tokens.cbBTC.decimals;    // 8
config.tokens.aBasWETH.address;  // '0xD4a0e0b9149BCee3C920d2E00b5dE09138fd8bb7'
config.tokens.aBascbBTC.address; // '0xBdb9300b7CDE636d9cD4AFF00f6F009fFBBc8EE6'
config.tokens.aBasUSDC.address;  // '0x4e65fE4DbA92790696d040ac24Aa414708F5c0AB'
config.tokens.cbDOGE.address;    // '0x73c7A9C372F31c1b1C7f8E5A7D12B8735c817C79'
config.tokens.cbXRP.address;     // '0x7B2Cd9EA5566c345C9cdbcF58f5E211a0dB47444'
```

## Option Implementations

### Cash-Settled

Used by both OptionBook (filling listed orders) and RFQ (creating custom options via `buildRFQRequest()`). The same implementation contracts are shared between both systems.

| Key                   | Address                                      | Description                            |
| --------------------- | -------------------------------------------- | -------------------------------------- |
| `PUT`                 | `0x7355EB92dfb0503DB558a70c10843618932ab290` | Vanilla PUT                            |
| `INVERSE_CALL`        | `0xE6c5756b0289e3f0994CB12eb8aB71Cd903Ed0Ea` | Vanilla CALL                           |
| `LINEAR_CALL`         | `0x051791df68223AE173Fade5217C48875e36eef61` | Linear CALL                            |
| `CALL_SPREAD`         | `0xfaeD63f7040E65b79cF0Ae29706fDc423eE249A9` | Call spread (2 strikes)                |
| `PUT_SPREAD`          | `0x02Fe0d9635e0139DBB3768a5d5Db404Fd84d9134` | Put spread (2 strikes)                 |
| `INVERSE_CALL_SPREAD` | `0x7Be48100b1B0349528A96D64953295Cd0Bbe4B70` | Inverse call spread (2 strikes)        |
| `CALL_FLY`            | `0xa1d5f6b16A2e7f298F8d2cDF78F7779B4A20C4C2` | Call butterfly (3 strikes)             |
| `PUT_FLY`             | `0x4fd2C6D271cC6FF3EbD2027da9815a0608d03AA3` | Put butterfly (3 strikes)              |
| `CALL_CONDOR`         | `0x14476CF2ea9F7C448100F061670E390f17c78817` | Call condor (4 strikes)                |
| `PUT_CONDOR`          | `0xC742E422c7BB43A7FDe1CEF47997bC9D5b543cDD` | Put condor (4 strikes)                 |
| `IRON_CONDOR`         | `0x9ebd7E23AfD52a48F557523019285EfEF2170D59` | Iron condor (4 strikes)                |
| `RANGER`              | `0x9980ec85bc6fE07340adb36c76FA093bb6D4FcBc` | Zone-bound 4-strike payoff (r12)       |
| `CALL_LOAN`           | `0x7c444A2375275DaB925b32493B64a407eE955DEd` | Loan handler (physically-settled call) |

### Physically Settled

Used by RFQ only, via `buildPhysicalOptionRFQ()`. Vanilla options only. At expiry, the underlying asset is delivered rather than a cash payout.

| Key             | Address                                      | Description           |
| --------------- | -------------------------------------------- | --------------------- |
| `PHYSICAL_CALL` | `0x8c56100caE246f7daa4BC1EC4d1477d71178c563` | Vanilla physical CALL |
| `PHYSICAL_PUT`  | `0x6aD53DD058bea004829cCf58a282C21a7Df02DcA` | Vanilla physical PUT  |

> Multi-leg physical implementations (`PHYSICAL_*_SPREAD`, `PHYSICAL_*_FLY`, `PHYSICAL_*_CONDOR`, `PHYSICAL_IRON_CONDOR`) are placeholders set to `0x0…0` — the contracts are not yet deployed. The SDK's runtime guard throws a clear error if a user attempts to route through any of these zero addresses.

```typescript
// Cash-settled
config.implementations.PUT;
config.implementations.INVERSE_CALL;
config.implementations.PUT_SPREAD;
config.implementations.CALL_SPREAD;
config.implementations.PUT_FLY;
config.implementations.CALL_FLY;
config.implementations.PUT_CONDOR;
config.implementations.CALL_CONDOR;
config.implementations.IRON_CONDOR;

// Physically settled (vanilla only)
config.implementations.PHYSICAL_CALL;
config.implementations.PHYSICAL_PUT;
```

## Price Feeds (Chainlink)

| Asset | Feed Address                                 |
| ----- | -------------------------------------------- |
| ETH   | `0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70` |
| BTC   | `0x64c911996D3c6aC71f9b455B1E8E7266BcbD848F` |
| SOL   | `0x975043adBb80fc32276CbF9Bbcfd4A601a12462D` |
| DOGE  | `0x8422f3d3CAFf15Ca682939310d6A5e619AE08e57` |
| XRP   | `0x9f0C1dD78C4CBdF5b9cf923a549A201EdC676D34` |
| BNB   | `0x4b7836916781CAAfbb7Bd1E5FDd20ED544B453b1` |
| PAXG  | `0x5213eBB69743b85644dbB6E25cdF994aFBb8cF31` |
| AVAX  | `0xE70f2D34Fd04046aaEC26a198A35dD8F2dF5cd92` |

```typescript
config.priceFeeds.ETH;   // Chainlink ETH/USD feed
config.priceFeeds.BTC;   // Chainlink BTC/USD feed
config.priceFeeds.SOL;   // Chainlink SOL/USD feed
config.priceFeeds.DOGE;  // Chainlink DOGE/USD feed
config.priceFeeds.XRP;   // Chainlink XRP/USD feed
config.priceFeeds.BNB;   // Chainlink BNB/USD feed
config.priceFeeds.PAXG;  // Chainlink PAXG/USD feed
config.priceFeeds.AVAX;  // Chainlink AVAX/USD feed
```

## Contracts

| Contract        | Address                                      |
| --------------- | -------------------------------------------- |
| `optionBook`    | `0x1bDff855d6811728acaDC00989e79143a2bdfDed` |
| `optionFactory` | `0x8118daD971dEbffB49B9280047659174128A8B94` |
| `twapConsumer`  | `0xE909fb38767e0ac5F7a347DF9Dd4222217E10816` |

```typescript
config.contracts.optionBook;      // '0x1bDff855d6811728acaDC00989e79143a2bdfDed'
config.contracts.optionFactory;   // '0x8118daD971dEbffB49B9280047659174128A8B94'
config.contracts.twapConsumer;    // '0xE909fb38767e0ac5F7a347DF9Dd4222217E10816'
```

## API Endpoints

| Field           | Purpose                         | Default URL                                           |
| --------------- | ------------------------------- | ----------------------------------------------------- |
| `apiBaseUrl`    | Orders API                      | `https://round-snowflake-9c31.devops-118.workers.dev` |
| `indexerApiUrl` | Book indexer (positions, stats) | `https://indexer.thetanuts.finance/api/v1/book`       |
| `pricingApiUrl` | Greeks and IV surfaces          | `https://pricing.thetanuts.finance`                   |
| `stateApiUrl`   | RFQ state indexer               | `https://indexer.thetanuts.finance`                   |
| `wsBaseUrl`     | WebSocket server                | `wss://ws.thetanuts.finance/v4`                       |

## ChainConfig Interface

```typescript
interface ChainConfig {
  chainId: number;
  name: string;

  contracts: {
    optionBook: string;
    optionFactory: string;
  };

  implementations: {
    PUT: string;
    INVERSE_CALL: string;
    CALL_SPREAD: string;
    PUT_SPREAD: string;
    CALL_FLY: string;
    PUT_FLY: string;
    CALL_CONDOR: string;
    PUT_CONDOR: string;
    IRON_CONDOR: string;
    // Physical implementations also included
  };

  tokens: {
    [symbol: string]: {
      address: string;
      symbol: string;
      decimals: number;
    };
  };

  priceFeeds: {
    ETH: string;
    BTC: string;
  };

  apiBaseUrl: string;
  indexerApiUrl: string;
  pricingApiUrl: string;
  wsBaseUrl: string;
  stateApiUrl: string;

  defaultRpcUrls: string[];
}
```

## Helper Functions

These exported functions let you access chain config without a client instance:

```typescript
import {
  getChainConfigById,
  getTokenConfigById,
  getSupportedTokensById,
  isChainIdSupported,
} from '@thetanuts-finance/thetanuts-client';

// Full config
const config = getChainConfigById(8453);
console.log(config.name);                    // 'Base'
console.log(config.contracts.optionBook);    // '0x1bDff855...'

// Single token
const usdc = getTokenConfigById(8453, 'USDC');
console.log(usdc.address);   // '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
console.log(usdc.decimals);  // 6

// All tokens
const tokens = getSupportedTokensById(8453);
// ['USDC', 'WETH', 'cbBTC', 'aBasWETH', 'aBascbBTC', 'aBasUSDC', 'cbDOGE', 'cbXRP']

// Chain support check
isChainIdSupported(8453);  // true (Base)
isChainIdSupported(1);     // true (Ethereum, vault-only)
```

## Never Hardcode Addresses

Always read addresses from `client.chainConfig` rather than hardcoding them. This ensures your code works correctly if addresses change in a future SDK release:

```typescript
// Preferred
const usdcAddress = client.chainConfig.tokens.USDC.address;
const optionBook  = client.chainConfig.contracts.optionBook;

// Then use in approvals and transactions
await client.erc20.ensureAllowance(usdcAddress, optionBook, amount);
```

***

## See Also

* [Decimal Reference](/sdk/sdk-reference/decimals) — Token decimal table
* [Client](/sdk/sdk-reference/client) — ThetanutsClient properties
* [../getting-started/configuration.md](/sdk/getting-started/configuration) — Getting started with configuration


# Decimal Reference

Token amounts and prices in the Thetanuts SDK are always passed as `bigint` values scaled to the token's native decimal precision. This page documents the decimal scale for every value type you will encounter.

## Decimal Table

| Type           | Decimals            | Constant         | Example                         |
| -------------- | ------------------- | ---------------- | ------------------------------- |
| USDC           | 6                   | `DECIMALS.USDC`  | `1000000n` = 1 USDC             |
| WETH           | 18                  | `DECIMALS.WETH`  | `1000000000000000000n` = 1 WETH |
| cbBTC          | 8                   | `DECIMALS.cbBTC` | `100000000n` = 1 cbBTC          |
| Strike / Price | 8                   | `DECIMALS.PRICE` | `185000000000n` = $1850         |
| numContracts   | collateral decimals | —                | Depends on the collateral token |

`numContracts` uses the same decimal precision as the position's collateral token. For a USDC-collateralized option, `numContracts` is expressed in 6 decimals; for a WETH-collateralized option, it is 18 decimals.

## Decimal Constants

```typescript
import { DECIMALS } from '@thetanuts-finance/thetanuts-client';

DECIMALS.USDC   // 6
DECIMALS.WETH   // 18
DECIMALS.cbBTC  // 8
DECIMALS.PRICE  // 8  — strikes and settlement prices
DECIMALS.SIZE   // 18 — numContracts (independent of collateral)
```

## Conversion Examples

The SDK provides helpers for all conversions. Use them instead of manual multiplication to avoid floating-point precision errors.

### To on-chain values

```typescript
import { ThetanutsClient } from '@thetanuts-finance/thetanuts-client';

const client = new ThetanutsClient({ chainId: 8453, provider });

// Generic: toBigInt(humanValue, decimals)
const usdc   = client.utils.toBigInt('100.5', 6);    // 100500000n
const weth   = client.utils.toBigInt('1.5', 18);     // 1500000000000000000n
const cbBTC  = client.utils.toBigInt('0.001', 8);    // 100000n

// USDC shorthand
const usdc2  = client.utils.toUsdcDecimals('100.5'); // 100500000n

// Strike price (8 decimals) — string-based to avoid float errors
const strike = client.utils.strikeToChain(1850.5);   // 185050000000n
const btcStrike = client.utils.strikeToChain(95000); // 9500000000000n
```

### From on-chain values

```typescript
// Generic: fromBigInt(bigintValue, decimals) -> string
const display = client.utils.fromBigInt(100500000n, 6);           // '100.5'
const eth     = client.utils.fromBigInt(1500000000000000000n, 18); // '1.5'

// USDC shorthand
const usdcStr = client.utils.fromUsdcDecimals(100500000n);        // '100.5'

// Strike price
const strikePx = client.utils.strikeFromChain(185050000000n);     // 1850.5
```

## Real-World Usage

### Approving collateral for an order fill

```typescript
// 10 USDC = 10_000000n (6 decimals)
await client.erc20.ensureAllowance(
  client.chainConfig.tokens.USDC.address,
  client.chainConfig.contracts.optionBook,
  10_000000n,
);
```

### Building an RFQ with a human-readable strike

```typescript
// Pass the number directly — buildRFQParams handles the conversion
const params = client.optionFactory.buildRFQParams({
  strikes: 1850,        // human-readable, no manual conversion needed
  numContracts: 1.5,    // also human-readable
  collateralToken: 'USDC',
  // ...
});
```

### Reading settlement prices from on-chain

Settlement prices and price feeds return values with 8 decimal places:

```typescript
// Settlement price: 200000000000n = $2000.00
const priceUsd = Number(200000000000n) / 1e8;  // 2000
// Or use the helper:
const priceStr = client.utils.strikeFromChain(200000000000n);  // 2000
```

### Formatting balances for display

```typescript
import { ethers } from 'ethers';

const usdcBalance = await client.erc20.getBalance(usdcAddress, userAddress);
console.log(ethers.formatUnits(usdcBalance, 6), 'USDC');

const wethBalance = await client.erc20.getBalance(wethAddress, userAddress);
console.log(ethers.formatUnits(wethBalance, 18), 'WETH');
```

## Common Mistakes

**Do not use JavaScript `number` arithmetic for on-chain values.** JavaScript's 64-bit float loses precision at large integers. Always use `bigint` or the SDK helpers:

```typescript
// WRONG — float precision error
const strike = 95000.5 * 1e8;  // may not equal 9500050000000

// CORRECT — string-based, exact
const strike = client.utils.strikeToChain(95000.5);  // 9500050000000n
```

**Do not hardcode decimal scales.** Use the `DECIMALS` constants or read `.decimals` from `client.chainConfig.tokens`:

```typescript
// WRONG
const amount = BigInt(1.5 * 1e6);  // could drift

// CORRECT
const amount = client.utils.toUsdcDecimals('1.5');  // 1500000n
```

***

## See Also

* [Utilities](/sdk/sdk-reference/utilities) — Full UtilsModule method reference
* [Chain Config](/sdk/sdk-reference/chain-config) — Token addresses and decimals per chain
* [Modules Overview](/sdk/sdk-reference/modules-overview) — All 15 modules at a glance


# LLM Context (use with Claude/Cursor/ChatGPT)

> **Want your AI assistant to know this SDK cold?** Paste the prompt below into Claude Code, Cursor, ChatGPT, or any LLM-powered tool. It pulls in the full SDK context in one fetch — every module, key types, common workflows, and the gotchas that bite real users.

## Copy-paste this prompt

```
Please fetch and read https://raw.githubusercontent.com/Thetanuts-Finance/thetanuts-sdk/main/llms-full.txt before answering any further questions about the Thetanuts Finance SDK or generating code that uses it. That file is the full LLM-targeted context for @thetanuts-finance/thetanuts-client — it covers every module, key types, common workflows with runnable examples, and the gotchas that cause real bugs.

Once you've read it, you can answer questions about the SDK directly without making additional doc fetches.
```

That's it. Copy, paste, and your LLM has every module on `client.*`, every type, every common workflow, and every "this will silently revert in production" gotcha — about 35 KiB of curated markdown.

## What's in `llms-full.txt`

Self-contained reference, designed to replace multiple page-fetches:

* **Architecture**: every public module on `ThetanutsClient`, what it does, when to use it
* **Key concepts**: OptionBook vs RFQ, decimal handling, collateral by option type
* **Per-module reference**: `client.optionBook`, `client.optionFactory`, `client.option`, `client.ranger`, `client.events`, `client.ws`, `client.utils`, `client.rfqKeys`, `client.mmPricing`, `client.loan`, `client.collar`, `client.wheelVault`, `client.strategyVault`, `client.api`, `client.erc20`
* **Common workflows**: read market data, fill an order, create a custom RFQ, manage positions, borrow USDC, deposit into a vault, real-time subscriptions
* **Error handling**: every typed `ThetanutsError` code and what triggers it
* **Gotchas**: the recurring footguns — `availableAmount` is collateral budget not contract count, `collateralAmount` in RFQ params is always 0, `split`/`reclaimCollateral` are payable in r12, butterfly names changed in v0.2.1, etc.
* **Contract addresses**: Base mainnet r12 deployment

## Two files, two audiences

The repo has two LLM-targeted files at the root:

| File                                                                                                    | Size     | Use case                                                                                                                         |
| ------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------- |
| [`llms.txt`](https://raw.githubusercontent.com/Thetanuts-Finance/thetanuts-sdk/main/llms.txt)           | \~8 KiB  | Curated **index** of canonical docs — link-only, grouped by topic. Use when your LLM wants to know which doc page to fetch next. |
| [`llms-full.txt`](https://raw.githubusercontent.com/Thetanuts-Finance/thetanuts-sdk/main/llms-full.txt) | \~35 KiB | **Full embedded context** — every module and the gotchas in one file. Fetch this first; cache for the session.                   |

Both follow the [llmstxt.org](https://llmstxt.org) spec, so any tool that auto-discovers LLM context files will find them.

## Other ways to consume

### Connect via the MCP server

If you're using Claude Desktop or any MCP-aware client, the SDK ships an MCP server that surfaces the same context as a tool call. Three context tools, plus SDK read tools and `prepare_*` calldata builders:

* `get_sdk_context` — full long-form context (same content as `llms-full.txt`)
* `get_sdk_context_index` — curated index (same content as `llms.txt`)
* `get_sdk_context_size` — byte size, for budgeting before fetch

See the [MCP Server guide](/sdk/ai-agents/mcp-server) for setup instructions.

### Direct fetch in your code

If you're building an LLM application that wraps Thetanuts, fetch `llms-full.txt` once at startup and inject it as a system message:

```typescript
const sdkContext = await fetch(
  'https://raw.githubusercontent.com/Thetanuts-Finance/thetanuts-sdk/main/llms-full.txt'
).then((r) => r.text());

// Then use sdkContext as a system message or inject before user prompts
```

The file is generated at the same time as the npm package, so what you see matches the latest published version.

## Versioning

The two LLM files track the latest published SDK release. To pin to a specific version, replace `main` in the URL with a version tag:

```
https://raw.githubusercontent.com/Thetanuts-Finance/thetanuts-sdk/v0.3.0/llms-full.txt
```

See [GitHub Releases](https://github.com/Thetanuts-Finance/thetanuts-sdk/releases) for the available tags.

## See also

* [GitHub: `llms.txt`](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/llms.txt) — the curated index, viewed in GitHub
* [GitHub: `llms-full.txt`](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/llms-full.txt) — the full context, viewed in GitHub
* [MCP Server](/sdk/ai-agents/mcp-server) — same content surfaced as MCP tool calls
* [llmstxt.org](https://llmstxt.org) — the spec these files follow


# Migration Guide

Upgrade guide for existing users moving to the latest SDK patterns and APIs.

## Table of Contents

* [Migrating to v0.3.0](#migrating-to-v030)
* [Migrating to v0.2.1 (Base\_r12 deployment)](#migrating-to-v021-base_r12-deployment)
* [Breaking Changes](#breaking-changes)
* [New Helper Methods](#new-helper-methods)
* [Code Migration Examples](#code-migration-examples)
* [Deprecation Notices](#deprecation-notices)
* [Indexer Migration Notes](#indexer-migration-notes)

***

## Migrating to v0.3.0

**No breaking changes** — 0.3.0 is additive over 0.2.x. Upgrade with a plain `npm install @thetanuts-finance/thetanuts-client@^0.3.0`. One heads-up between the 0.2.x patches: v0.2.3 renamed two `strategyVault` symbols (see [Changelog](/sdk/resources/changelog)) — if you're jumping from ≤0.2.2, apply that find-and-replace.

What you gain:

* **RFQ sealed-bid agent helpers** — `optionFactory.buildOfferTypedData()`, `api.getRequesterPublicKey()`, `api.getOffer()`. Required by [`@thetanuts-finance/agentkit`](/sdk/ai-agents/agentkit) (its peer range is `>=0.3.0`).
* **Off-chain multi-leg payout/collateral math** — `client.utils.calculatePayout()` / `calculateCollateral()` for flies, condors, iron condors, and rangers (previously `INVALID_PARAMS`).
* **Hardened write paths** — pre-write network assertions and stricter swap-parameter validation. Code that previously passed malformed swap params to `swapAndFillOrder` / `marketFill` now fails fast with `INVALID_PARAMS` instead of reverting on-chain.

## Migrating to v0.2.1 (Base\_r12 deployment)

v0.2.1 is the first 0.2.x release published to npm. It bundles the Base\_r12 deployment cutover with 22 fixes that three adversarial code-review passes found in the staged surface. v0.2.0 was prepared internally but never published — there is no v0.2.0 on npm.

### Pin to the deployment you want

* `@thetanuts-finance/thetanuts-client@^0.1.x` — prior Base deployment (immutable on npm; use only if you need to keep talking to the old contracts).
* `@thetanuts-finance/thetanuts-client@^0.2.1` — Base\_r12 deployment (recommended).

### Breaking changes from 0.1.x → 0.2.1

* **`client.events.getCollateralReturnedEvents` was renamed to `getExcessCollateralReturnedEvents`** with a new field shape:

  ```typescript
  // ❌ OLD (0.1.x):
  const events = await client.events.getCollateralReturnedEvents(option);
  // events[i] had { optionAddress, seller, amountReturned }

  // ✅ NEW (0.2.1):
  const events = await client.events.getExcessCollateralReturnedEvents(option);
  // events[i] has { seller, collateralToken, collateralReturned }
  ```
* **`OptionSplitEvent` gained two fields.** If you were destructuring, add `feePaid` and `counterparty`:

  ```typescript
  // ❌ OLD: { newOption, collateralAmount }
  // ✅ NEW: { newOption, collateralAmount, feePaid, counterparty }
  ```
* **`TransferApprovalEvent` field order is fixed** to match r12: `{ target, from, isBuyer, isApproved }`. The 0.1.x SDK had `target` and `from` swapped in the typed wrapper for `RangerOption` events specifically.
* **`OptionSettlementFailedEvent` no longer has fields** at the contract level. The SDK still populates `optionAddress` from the filter context for caller convenience.
* **Butterfly reverse-lookup names reconciled.** `getOptionImplementationInfo(addr).name` returns `'CALL_FLY'` / `'PUT_FLY'` instead of `'CALL_FLYS'` / `'PUT_FLYS'`. The `ProductName` union in `client.utils` follows.
* **`RangerModule.reclaimCollateral` parameter renamed** from `recipient` to `ownedOption`. Positional callers unaffected; named callers using a TypeScript object-shape break.
* **`LoanRequest.keepOrderOpen` is now a no-op.** The r12 contract dropped the limit-order conversion field. The SDK still accepts the field for source compatibility but the value is ignored. The internal `LoanCoordinator.requestLoan` parameter tuple no longer includes `convertToLimitOrder` either; the SDK no longer sends it.
* **`LoanIndexerLoan.convertToLimitOrder` is optional.** The lender-opportunity filter at `client.loan.getLendingOpportunities` only skips when the field is explicitly `false`; missing means eligible. If the r12 indexer drops the field entirely, your lender opportunity list still populates.

### What you should also know about 0.2.1

* New `client.ranger` module — see the [Modules Overview](/sdk/sdk-reference/modules-overview#clientranger--rangermodule).
* `chainConfig.implementations.RANGER`, `LINEAR_CALL`, `INVERSE_CALL_SPREAD`, `CALL_LOAN` are new.
* `chainConfig.twapConsumer` (string | null) surfaces the HistoricalPriceConsumerV3\_TWAP address.
* Ethereum mainnet (`chainId 1`) is now supported as a vault-only chain (only `client.wheelVault` works there).
* Every RFQ entry point now rejects the seven undeployed `PHYSICAL_*` placeholder addresses with a clear `INVALID_PARAMS` error.
* `RANGER_OPTION_ABI` is exported from the package root.

***

## Breaking Changes

### collateralAmount Parameter

**The most critical change:** `collateralAmount` must now **always be 0** when creating RFQs.

#### Before (Incorrect Pattern)

```typescript
// ❌ OLD: Calculating and passing collateralAmount
const strike = 1850;
const numContracts = 1.5;
const collateralAmount = BigInt(Math.round(strike * numContracts * 1e6));

const params = {
  // ...
  collateralAmount: collateralAmount,  // WRONG!
  // ...
};
```

#### After (Correct Pattern)

```typescript
// ✅ NEW: collateralAmount is always 0
const params = {
  // ...
  collateralAmount: BigInt(0),  // ALWAYS 0
  // ...
};

// Or use buildRFQParams which enforces this automatically
const params = client.optionFactory.buildRFQParams({
  // ... collateralAmount is set to 0 internally
});
```

#### Why This Changed

Collateral is **NOT** locked at RFQ creation time. It is pulled from both parties at **settlement** time. The `collateralAmount` parameter in RFQ was misleading and should always be 0.

**For SELL positions:** You still need to calculate collateral, but only for **token approval**:

```typescript
// For SELL positions, approve tokens before creating RFQ
const strike = 1850;
const numContracts = 1.5;

// PUT: approval = strike * numContracts
const approval = BigInt(Math.round(strike * numContracts * 1e6));
await client.erc20.approve(USDC, optionFactoryAddress, approval);

// Then create RFQ with collateralAmount = 0
const params = client.optionFactory.buildRFQParams({
  isLong: false,  // SELL
  // ...
});
```

***

## New Helper Methods

### buildRFQParams()

High-level RFQ builder that:

* **Enforces `collateralAmount = 0`** automatically
* Resolves addresses from chain config (no hardcoding)
* Handles decimal conversions

```typescript
// Before: Manual address lookup and decimal handling
const params = {
  requester: userAddress,
  existingOptionAddress: '0x0000000000000000000000000000000000000000',
  collateral: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',  // Hardcoded USDC
  collateralPriceFeed: '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70',  // Hardcoded
  implementation: '0xF480F636301d50Ed570D026254dC5728b746A90F',  // Hardcoded
  strikes: [BigInt(1850) * BigInt(1e8)],
  numContracts: BigInt(Math.round(1.5 * 1e6)),
  // ... more manual conversions
};

// After: Clean, type-safe builder
const params = client.optionFactory.buildRFQParams({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  strike: 1850,           // Human-readable
  numContracts: 1.5,      // Human-readable
  expiry: 1741334400,
  isLong: true,
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
});
```

### buildRFQRequest()

Complete RFQ request builder including tracking and reserve price:

```typescript
const request = client.optionFactory.buildRFQRequest({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  strike: 1850,
  expiry: 1741334400,
  numContracts: 1.5,
  isLong: true,
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
  reservePrice: 0.015,        // Optional: max per-contract price
  requesterPublicKey: '0x...', // ECDH public key
});

const { to, data } = client.optionFactory.encodeRequestForQuotation(request);
```

### strikeToChain() / strikeFromChain()

Precision-safe strike conversion using string-based parsing (avoids floating-point errors):

```typescript
// Before: Potential floating-point issues
const strike = BigInt(Math.round(1850.5 * 1e8));  // May have precision errors

// After: Safe conversion
const strike = client.utils.strikeToChain(1850.5);  // 185050000000n
const display = client.utils.strikeFromChain(185050000000n);  // 1850.5
```

### getFullOptionInfo()

Aggregated option info in a single call (replaces multiple Promise.all patterns):

```typescript
// Before: Multiple individual calls
const [info, buyer, seller, isExpired, isSettled, numContracts, collateralAmount] =
  await Promise.all([
    client.option.getOptionInfo(optionAddress),
    client.option.getBuyer(optionAddress),
    client.option.getSeller(optionAddress),
    client.option.isExpired(optionAddress),
    client.option.isSettled(optionAddress),
    client.option.getNumContracts(optionAddress),
    client.option.getCollateralAmount(optionAddress),
  ]);

// After: Single aggregated call
const fullInfo = await client.option.getFullOptionInfo(optionAddress);
console.log(fullInfo.info);
console.log(fullInfo.buyer);
console.log(fullInfo.seller);
console.log(fullInfo.isExpired);
console.log(fullInfo.isSettled);
console.log(fullInfo.numContracts);
console.log(fullInfo.collateralAmount);
```

### MM Pricing Filter Utilities

New utilities for filtering and sorting pricing data:

```typescript
const all = await client.mmPricing.getAllPricing('ETH');
const values = Object.values(all);

// Filter expired
const active = client.mmPricing.filterExpired(values);

// Sort by expiry then strike
const sorted = client.mmPricing.sortByExpiryAndStrike(values);

// Get unique expiries
const expiries = client.mmPricing.getUniqueExpiries(values);

// Filter by type
const puts = client.mmPricing.filterByType(values, false);

// Filter by expiry
const feb16 = client.mmPricing.filterByExpiry(values, '2025-02-16');

// Filter by strike range
const nearATM = client.mmPricing.filterByStrikeRange(values, 1800, 2200);

// Convenience: sorted, non-expired array
const pricing = await client.mmPricing.getPricingArray('ETH');
```

***

## Code Migration Examples

### Example 1: RFQ Creation for BUY Position

```typescript
// ========== BEFORE ==========
const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
const PUT_IMPL = '0xF480F636301d50Ed570D026254dC5728b746A90F';
const ETH_FEED = '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70';

const strike = 1850;
const numContracts = 1.5;
const strikeScaled = BigInt(strike) * BigInt(1e8);
const numContractsScaled = BigInt(Math.round(numContracts * 1e6));

const params = {
  requester: userAddress,
  existingOptionAddress: ethers.ZeroAddress,
  collateral: USDC,
  collateralPriceFeed: ETH_FEED,
  implementation: PUT_IMPL,
  strikes: [strikeScaled],
  numContracts: numContractsScaled,
  requesterDeposit: BigInt(0),
  collateralAmount: BigInt(1000000),  // WRONG - was calculated
  expiryTimestamp: BigInt(expiry),
  offerEndTimestamp: BigInt(Math.floor(Date.now() / 1000) + 3600),
  isRequestingLongPosition: true,
  convertToLimitOrder: false,
  extraOptionData: '0x',
};

const { to, data } = client.optionFactory.encodeRequestForQuotation({
  params,
  tracking: { referralId: BigInt(0), eventCode: BigInt(0) },
  reservePrice: BigInt(0),
  requesterPublicKey: publicKey,
});

// ========== AFTER ==========
const request = client.optionFactory.buildRFQRequest({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  strike: 1850,
  numContracts: 1.5,
  expiry: expiry,
  isLong: true,
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
  requesterPublicKey: publicKey,
});

const { to, data } = client.optionFactory.encodeRequestForQuotation(request);
```

### Example 2: RFQ Creation for SELL Position

```typescript
// ========== BEFORE ==========
// Calculate approval manually
const strike = 1850;
const numContracts = 1.5;
const approval = BigInt(Math.round(strike * numContracts * 1e6));

await client.erc20.approve(USDC, optionFactory, approval);

const params = {
  // ... same as BUY but with incorrect collateralAmount
  collateralAmount: approval,  // WRONG!
  isRequestingLongPosition: false,
};

// ========== AFTER ==========
// Step 1: Approve tokens (still required for SELL)
const strike = 1850;
const numContracts = 1.5;
const approval = BigInt(Math.round(strike * numContracts * 1e6));
await client.erc20.approve(
  client.chainConfig.tokens.USDC.address,
  client.optionFactory.contractAddress,
  approval
);

// Step 2: Build RFQ (collateralAmount = 0 enforced automatically)
const request = client.optionFactory.buildRFQRequest({
  requester: userAddress,
  underlying: 'ETH',
  optionType: 'PUT',
  strike: 1850,
  numContracts: 1.5,
  expiry: expiry,
  isLong: false,  // SELL
  offerDeadlineMinutes: 60,
  collateralToken: 'USDC',
  requesterPublicKey: publicKey,
});

const { to, data } = client.optionFactory.encodeRequestForQuotation(request);
```

### Example 3: Option Position Queries

```typescript
// ========== BEFORE ==========
// Multiple separate calls
const buyer = await client.option.getBuyer(optionAddress);
const seller = await client.option.getSeller(optionAddress);
const isExpired = await client.option.isExpired(optionAddress);
const numContracts = await client.option.getNumContracts(optionAddress);

// Or Promise.all pattern
const [buyer, seller, isExpired, numContracts] = await Promise.all([
  client.option.getBuyer(optionAddress),
  client.option.getSeller(optionAddress),
  client.option.isExpired(optionAddress),
  client.option.getNumContracts(optionAddress),
]);

// ========== AFTER ==========
// Single aggregated call
const info = await client.option.getFullOptionInfo(optionAddress);

console.log(info.buyer);
console.log(info.seller);
console.log(info.isExpired);
console.log(info.numContracts);
```

### Example 4: Strike Price Conversion

```typescript
// ========== BEFORE ==========
// Manual conversion with potential precision issues
const strikeNumber = 1850.5;
const strikeOnChain = BigInt(Math.round(strikeNumber * 1e8));

// Parsing back
const strikeDisplay = Number(strikeOnChain) / 1e8;

// ========== AFTER ==========
// Safe string-based conversion
const strikeOnChain = client.utils.strikeToChain(1850.5);
const strikeDisplay = client.utils.strikeFromChain(strikeOnChain);
```

***

## Deprecation Notices

### Deprecated Patterns

| Pattern                               | Status     | Replacement                                  |
| ------------------------------------- | ---------- | -------------------------------------------- |
| Hardcoded contract addresses          | Deprecated | Use `client.chainConfig`                     |
| Manual `collateralAmount` calculation | Deprecated | Always use `BigInt(0)` or `buildRFQParams()` |
| Multiple Promise.all for option info  | Deprecated | Use `getFullOptionInfo()`                    |
| Manual strike scaling                 | Deprecated | Use `strikeToChain()` / `strikeFromChain()`  |

### API Method Renames

To clarify data sources, some API methods have been renamed:

| Old Method           | New Method                      | Data Source   |
| -------------------- | ------------------------------- | ------------- |
| `getUserPositions()` | `getUserPositionsFromIndexer()` | Indexer API   |
| `getUserHistory()`   | `getUserHistoryFromIndexer()`   | Indexer API   |
| `getStats()`         | `getStatsFromIndexer()`         | Indexer API   |
| `getUserRFQs()`      | `getUserRFQsFromRfq()`          | State/RFQ API |
| `getRFQ()`           | `getRFQFromRfq()`               | State/RFQ API |

The old method names are still available as aliases but may be removed in a future version.

***

## Indexer Migration Notes

> Tested 2026-04-07 against `indexer.thetanuts.finance` (new unified indexer). Compared with `optionbook-indexer.thetanuts.finance` (old indexer, to be decommissioned).

### Open Issues

#### Issue 1: Referrer `userDailyMetrics` not implemented

* **Endpoint**: `GET /api/v1/book/referrer/:addr/state`
* **Severity**: Medium
* **Description**: Old indexer returns per-user, per-day trading stats. New indexer returns `{}`.
* **SDK impact**: `ReferrerStats.userDailyMetrics` in `src/types/api.ts` expects this data.

#### Issue 2: Referrer `topProfitableTrades` not implemented

* **Endpoint**: `GET /api/v1/book/referrer/:addr/state`
* **Severity**: Medium
* **Description**: Old indexer returns top 100 trades ranked by % return. New indexer returns `[]`.
* **SDK impact**: `ReferrerStats.topProfitableTrades` in `src/types/api.ts` expects this data.

#### Issue 4: Pagination not working on `/api/v1/factory/rfqs`

* **Endpoint**: `GET /api/v1/factory/rfqs?limit=2&offset=0`
* **Severity**: Low
* **Description**: Pagination params are ignored — all RFQs are returned regardless. No pagination metadata in response.
* **SDK impact**: None currently, but will not scale past \~5k RFQs.

#### Issue 6: Old indexer DNS records still active

* **Severity**: Low
* `optionbook-indexer.thetanuts.finance` still resolves and serves stale data.
* Cleanup pending once migration is fully verified.

### Resolved Issues

| Issue                                                         | Resolution                                                                       |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| SDK `ReferrerStats` type missing `summary` field              | Fixed — `ReferrerStats` now includes optional `summary?: ReferrerSummary`        |
| Daily stats endpoints return empty data                       | Fixed — `getBookDailyStats()`, `getFactoryDailyStats()`, `getDailyStats()` added |
| Factory referrals missing / empty                             | Fixed — `StateReferral` type corrected to match actual API response shape        |
| Active filter bug — cancelled RFQ appearing in active results | Fixed — SDK now applies client-side re-filtering as a safety net                 |

### New Endpoints (not yet in SDK)

The following endpoints are live on the new indexer but do not yet have dedicated SDK methods:

| Endpoint                             | Description                                                                    |
| ------------------------------------ | ------------------------------------------------------------------------------ |
| `GET /api/v1/book/stats/protocol`    | Book protocol stats with time windows and implementation-type breakdowns       |
| `GET /api/v1/factory/stats/protocol` | Factory protocol stats including `avgTimeToFill`, `avgOffersPerRfq`            |
| `GET /api/v1/stats/protocol`         | Combined book + factory protocol stats (merged totals + individual breakdowns) |

### Endpoint Status Summary

| Endpoint                               | Status                                                                              |
| -------------------------------------- | ----------------------------------------------------------------------------------- |
| `/api/v1/book/user/:addr/positions`    | Working                                                                             |
| `/api/v1/book/user/:addr/history`      | Working                                                                             |
| `/api/v1/book/stats`                   | Working                                                                             |
| `/api/v1/book/referrer/:addr/state`    | Partial (positions + summary; `userDailyMetrics` and `topProfitableTrades` missing) |
| `/api/v1/book/stats/protocol`          | Working (new)                                                                       |
| `/api/v1/book/stats/daily`             | Working — SDK: `getBookDailyStats()`                                                |
| `/api/v1/factory/rfqs`                 | Working (no pagination)                                                             |
| `/api/v1/factory/rfqs/:id`             | Working                                                                             |
| `/api/v1/factory/offers`               | Working                                                                             |
| `/api/v1/factory/options`              | Working                                                                             |
| `/api/v1/factory/stats`                | Working                                                                             |
| `/api/v1/factory/user/:addr/rfqs`      | Working                                                                             |
| `/api/v1/factory/user/:addr/offers`    | Working                                                                             |
| `/api/v1/factory/user/:addr/positions` | Working                                                                             |
| `/api/v1/factory/option/:addr`         | Working                                                                             |
| `/api/v1/factory/stats/protocol`       | Working (new)                                                                       |
| `/api/v1/factory/stats/daily`          | Working — SDK: `getFactoryDailyStats()`                                             |
| `/api/v1/book/option/:addr`            | Working                                                                             |
| `/api/v1/book/state`                   | Working                                                                             |
| `/api/v1/stats/protocol`               | Working (new)                                                                       |
| `/api/v1/stats/daily`                  | Working — SDK: `getDailyStats()`                                                    |
| `/api/state`                           | Working                                                                             |
| `/health`                              | Working                                                                             |

***

## See Also

* [Changelog](/sdk/resources/changelog) - Version history and release notes
* [Examples](/sdk/resources/examples) - Runnable code examples covering common patterns
* [Error Handling](/sdk/guides/error-handling) — error codes and recovery patterns


# Examples

Runnable TypeScript examples covering the most common SDK workflows.

## Overview

All examples live in the [`docs/examples/`](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/docs/examples/README.md) directory. Each file is self-contained and can be run directly with `ts-node` or adapted into your own integration.

***

## Example Files

### [fill-order.ts](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/docs/examples/fill-order.ts)

Complete OptionBook fill flow with preview, approval, and error handling.

Covers: previewing a fill, checking and approving token allowances, executing the fill transaction, and handling common error cases such as insufficient balance and expired orders.

***

### [claim-fees.ts](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/docs/examples/claim-fees.ts)

Check and claim referrer fees across all collateral tokens.

Covers: fetching all claimable fee balances per collateral token using `getAllClaimableFees()`, filtering tokens with non-zero balances, and batching claims with `claimAllFees()`.

***

### [create-rfq.ts](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/docs/examples/create-rfq.ts)

Complete RFQ creation flow for both BUY and SELL directions.

Covers: building RFQ parameters with `buildRFQRequest()`, approving collateral tokens for SELL positions, encoding and submitting the `requestForQuotation` transaction, and polling for incoming offers.

***

### [physical-option-rfq.ts](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/docs/examples/physical-option-rfq.ts)

Physically settled option RFQ (vanilla options only).

Covers: creating an RFQ for a physically settled vanilla option, setting `extraOptionData` for physical settlement, and the differences in collateral handling compared to cash-settled options.

***

### [fetch-pricing.ts](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/docs/examples/fetch-pricing.ts)

MM pricing retrieval with filters.

Covers: fetching all pricing with `getAllPricing()`, filtering by expiry, strike range, and option type using the built-in filter utilities, and displaying a sorted pricing table.

***

### [option-management.ts](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/docs/examples/option-management.ts)

Option queries and operations.

Covers: fetching full option info with `getFullOptionInfo()`, checking expiry and settlement status, querying buyer and seller addresses, and performing settlement or exercise transactions.

***

### [query-stats.ts](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/docs/examples/query-stats.ts)

Protocol and referrer statistics.

Covers: fetching combined protocol stats, querying referrer-specific stats including volume and fee breakdowns, and reading daily time-series data via `getDailyStats()`, `getBookDailyStats()`, and `getFactoryDailyStats()`.

***

## Running an Example

```bash
# Install dependencies
npm install

# Run a specific example (requires ts-node)
npx ts-node docs/examples/fetch-pricing.ts
```

Most examples require a configured `ThetanutsClient`. See [Quick Start](/sdk/getting-started/quick-start) for setup instructions.

***

## See Also

* [Quick Start](/sdk/getting-started/quick-start) - Set up your first client instance
* [Migration Guide](/sdk/resources/migration-guide) - Upgrade from older SDK patterns
* [Changelog](/sdk/resources/changelog) - Version history


# Changelog

Version history for the Thetanuts Finance SDK.

## Current Version

**v0.3.0** — [View all releases on GitHub](https://github.com/Thetanuts-Finance/thetanuts-sdk/releases)

Companion packages: [`@thetanuts-finance/mcp`](https://www.npmjs.com/package/@thetanuts-finance/mcp) **v1.0.0** (MCP server) and [`@thetanuts-finance/agentkit`](https://www.npmjs.com/package/@thetanuts-finance/agentkit) **v0.2.x** (autonomous agents) — see [AI Agents](/sdk/ai-agents/agents-overview).

This SDK follows [Semantic Versioning](https://semver.org/): `MAJOR.MINOR.PATCH`. Patch releases contain bug fixes and non-breaking improvements. Minor releases add new functionality in a backwards-compatible manner. Major releases may contain breaking changes and will be accompanied by a migration guide.

> **Where to look for release notes:**
>
> * [GitHub Releases](https://github.com/Thetanuts-Finance/thetanuts-sdk/releases) — canonical per-version detail (commit log, before/after diffs, verification steps).
> * [`CHANGELOG.md`](https://github.com/Thetanuts-Finance/thetanuts-sdk/blob/main/CHANGELOG.md) in the repo — terse per-version log, ships with the npm package.
> * The summaries below are the user-facing highlights for the latest few releases.

***

## Release History

### v0.3.0 — RFQ agent helpers, multi-leg payout math, hardening

Minor release, no breaking changes.

**Added:**

* **RFQ sealed-bid agent helpers** — `optionFactory.buildOfferTypedData()` (EIP-712 `Offer` envelope with live `OFFER_TYPEHASH` verification, fails closed on drift), `api.getRequesterPublicKey(quotationId)`, and `api.getOffer(...)`. These power [`@thetanuts-finance/agentkit`](/sdk/ai-agents/agentkit) and the MCP prepare service; agentkit's peer range requires `>=0.3.0`.
* **Off-chain payout + collateral math for all multi-leg structures.** `client.utils.calculatePayout()` / `calculateCollateral()` now cover `call_fly`, `put_fly`, `call_condor`, `put_condor`, `iron_condor`, and `ranger` — pure bigint, no RPC, usable for pre-trade UI previews. Previously multi-leg types threw `INVALID_PARAMS`.

**Fixed:** MCP `validate_ranger` schema matched to the SDK validator (4 strikes); `calculateMaxPayout` / `calculatePayoutAtPrice` no longer fall through to spread math for 3- and 4-strike orders.

**Security:** write methods assert the connected network before building transactions; OptionBook swap paths validate router/token addresses and swap data; MCP prepare tools tighten auth and redact URLs from errors.

### v0.2.5 — loan indexer r12 fixes

* Loan indexer URL repointed to the r12 worker — consumers were reading r12 contract state but querying the legacy v1 indexer, seeing archived loans and missing every r12 RFQ/offer/loan.
* `LoanModule` backfills the r12 indexer's missing fields (`strike`, `expiryTimestamp`, `buyer`, `seller`) from on-chain `getOptionInfo()`, so `getLendingOpportunities()` and `getUserLoans()` return complete rows.
* 9 collar types re-exported from the package barrel (they shipped in 0.2.4 but weren't reachable from the public API).

### v0.2.4 — Collar Loan module + security audit closeout

* **`client.collar`** — zero-interest, capped-upside loans via RFQ (buy put at `K_lo` + sell call at `K_hi`; MM funds an up-front USDC loan from the call premium). Pricing/read methods work today against live Deribit quotes; write methods throw `NETWORK_UNSUPPORTED` until the coordinator contract deploys.
* **Security audit closeout** — remediates the full `SECURITY_AUDIT_BETA.md` backlog (1 Critical, 24 High, 73 Medium/Low/Informational): admin-only entrypoints removed from SDK ABIs, WheelVault allowance + event-log filtering fixes, `marketFill` swap-router validation made unconditional, `splitOption`/`reclaimCollateral` fee-forwarding wrappers, and more — see the per-finding tracker in the repo.

### v0.2.3 — strategyVault rename (BREAKING)

Renames two public symbols on `client.strategyVault`. Behavior, ABIs, and contract addresses are unchanged.

> **Heads up:** despite the patch-level version bump, the two API renames below are **breaking**. Anyone using the previous config field or method names from v0.2.2 must update on upgrade. There are no deprecated aliases — old names are gone.

**Breaking renames:**

```diff
- const vault = STRATEGY_VAULT_CONFIG.<old>.vaults[0].address;
+ const vault = STRATEGY_VAULT_CONFIG.fixedStrike.vaults[0].address;

- const vaults = await client.strategyVault.<old>();
+ const vaults = await client.strategyVault.getFixedStrikeVaults();
```

All sub-fields of `STRATEGY_VAULT_CONFIG.<old>` (`vaults`, `baseAsset`, `quoteAsset`, `oracle`) move with the rename. Migration is a straight find-and-replace; return shapes are identical.

**Prose updates:** comments, JSDoc, runtime error messages, and docs use neutral terms ("fixed-strike", "wheel strategy") throughout. No symbol changes beyond the two breaking renames above.

**Test runner:** `scripts/run-mainnet-tests.ts` now retries transient `CALL_EXCEPTION` errors from public RPCs (the public Base RPC drops bursts of read calls; the test suite was failing 28/30 instead of 30/30 because of dropped responses, not contract bugs). Runner accepts a `BASE_RPC_URL` env var to override the public default.

**Unchanged:** `STRATEGY_VAULT_CONFIG.clvex`, `getClvexVaults()`, `getAllVaults()`, all on-chain contract addresses, ABIs, and module shapes.

### v0.2.2 — DX polish

Polish release with no new features and no breaking changes. Came out of a live `/devex-review` audit.

**Bug fixes:**

* `mapContractError` no longer clobbers typed `ThetanutsError` instances (e.g. `SIGNER_REQUIRED` from `requireSigner()`). Calling `client.optionBook.claimFees(token)` without a signer now reports `code: 'SIGNER_REQUIRED'` instead of generic `code: 'CONTRACT_REVERT'`.
* `NETWORK_UNSUPPORTED` error message now derives the supported-chains list dynamically from `CHAIN_CONFIGS_BY_ID`. Previous hardcoded string `"Supported chains: 8453 (Base)"` omitted Ethereum (added in v0.2.1).

**Project hygiene:**

* New `CONTRIBUTING.md` documents local setup, the four required gates, the `/codex review` + `/codex challenge` review process, and the npm publish flow.
* New `SECURITY.md` — vulnerability reporting policy and supported-versions table.
* New `.github/ISSUE_TEMPLATE/{bug_report,feature_request,question,config}.yml` and `PULL_REQUEST_TEMPLATE.md`.

**Documentation:**

* Fixed broken link in `docs/resources/migration-guide.md` (pointed at non-existent `reference/error-codes.md`; now points at `guides/error-handling.md`).
* Backfilled v0.1.x history in repo-root `CHANGELOG.md`.

### v0.2.1 — Base\_r12 deployment + codex-found fixes

The first 0.2.x release published to npm. Bundles the **Base\_r12 deployment cutover** with **22 fixes** that three adversarial code-review passes found in the staged surface. v0.2.0 was prepared internally but never published; everything ships in this single release.

> See the [v0.2.1 GitHub Release](https://github.com/Thetanuts-Finance/thetanuts-sdk/releases/tag/v0.2.1) for per-commit detail, before/after migration code, and verification steps.

**Base\_r12 cutover:**

* All chainId-8453 contract addresses point at the r12 deployment (`optionBook`, `optionFactory`, all 13 implementations, LoanCoordinator, LoanHandler).
* `deploymentBlock` → `45601440` (deployed 2026-05-05).
* Historical reverse-lookup entries (`8453_v6`, `Base_r10`) preserved so events emitted before the cutover still decode.
* Ethereum mainnet (`chainId 1`) added as a vault-only chain.

**New surface:**

* New `client.ranger` module — RangerOption (zone-bound, 4-strike payoff). Module is chain-gated; throws `NETWORK_UNSUPPORTED` on chains where RangerOption is not deployed (Ethereum mainnet today).
* `RANGER_OPTION_ABI` exported from the package root.
* `chainConfig.twapConsumer` (HistoricalPriceConsumerV3\_TWAP) surfaced as a top-level chain-config field.
* New chain-config implementation keys: `RANGER`, `LINEAR_CALL`, `INVERSE_CALL_SPREAD`, `CALL_LOAN`.
* New OptionBook surface: `cancelOrders`, `cancelOrdersExpiringBefore`, `getValidNumContracts`, `makerCancellationCutoff`, `minNumContracts`, `minPremiumAmount` + `MakerCutoffUpdated` event.
* New OptionFactory surface: `claimEscrowedFunds`, `claimableTransfers`, `totalClaimableTransfers`, `activeRfqForOption`, `baseSplitFee`, `MAX_TRANSFER_DUST`, `MAX_ORACLE_STALENESS`, `settleQuotationEarlyByOrderBook`, `historicalTWAPConsumer`, `deprecationTime`, `settlementExtension` + 10 new events.

**Production-revert fixes** (would have silently reverted once the protocol enabled non-zero fees):

* `split` and `reclaimCollateral` declared `payable`; the SDK forwards `getSplitFee()` / `getReclaimFee(ownedOption)` as `msg.value`.
* `RangerModule.reclaimCollateral` passes the option being reclaimed (not the caller's address) to `getReclaimFee`. The fee is keyed on the option, not the caller.

**ABI shape corrections** (against canonical r12 JSONs):

* `OptionBook.getValidNumContracts` returns the canonical tuple `result { validContracts, collateralRequired }`, not a single `uint256`.
* `BaseOption.optionType` is `view returns (uint256)`; `RangerOption.optionType` is `pure returns (uint256)`.
* `BaseOption.returnExcessCollateral` declares its `uint256` return.
* `LoanCoordinator.assetConfigs` returns the four-field tuple.

**Event shape corrections:**

* `OptionInitialized` (11 fields), `OptionSplit` (adds `feePaid` and `counterparty`), `TransferApproval` (field order corrected), `OptionSettlementFailed` (no inputs), and `ExcessCollateralReturned` (renamed from `CollateralReturned`) all match r12.
* `client.events.getCollateralReturnedEvents` is **`getExcessCollateralReturnedEvents`** with field shape `{ seller, collateralToken, collateralReturned }`.

**Safety upgrades:**

* All four RFQ entry points (`requestForQuotation`, `encodeRequestForQuotation`, `registerReferral`, `callStaticCreateRFQ`) reject the seven undeployed `PHYSICAL_*_SPREAD/FLY/CONDOR/IRON_CONDOR` zero-address placeholders with `INVALID_PARAMS` before any transaction is built.

**Loan changes:**

* `LOAN_COORDINATOR_ABI` updated for r12: `requestLoan` no longer carries `convertToLimitOrder`; `loanRequests` returns `loanClaimed`; new `LoanClaimed` event.
* `LoanRequest.keepOrderOpen` is `@deprecated` — the r12 contract ignores the value.
* `getLendingOpportunities` filter treats a missing `convertToLimitOrder` indexer field as eligible — only skips when explicitly `false`.

**Naming reconciliation:**

* `getOptionImplementationInfo(addr).name` for butterflies returns `'CALL_FLY'` / `'PUT_FLY'` (was `'CALL_FLYS'` / `'PUT_FLYS'`).
* `OptionImplementationInfo.type` union: `'RANGE'` replaced by `'RANGER'`.

**Breaking changes from v0.1.x:**

* `client.events.getCollateralReturnedEvents` removed; replaced by `getExcessCollateralReturnedEvents` with new fields.
* `OptionSplitEvent` adds `feePaid` and `counterparty`.
* `getOptionImplementationInfo(addr).name` for butterflies renamed (above).
* `LoanCoordinator.requestLoan` no longer accepts `convertToLimitOrder`.

### v0.1.6

* Added `getAllClaimableFees()` and `claimAllFees()` helpers on `optionBook` for batch fee claiming across all collateral tokens.

### v0.1.5

* Added `getFactoryReferrerStats()` for the `/factory/referrer/:address/state` endpoint.
* Narrowed catch-block errors from `any` to `unknown` for stricter type safety.

### v0.1.4

* Added Yarn Classic (v1) and Yarn Berry (v2+) publish support.
* Fixed `numContracts` precision handling and `existingOptionAddress` parameter defaults.
* Fixed `LINEAR_CALL` max contracts calculation.
* Fixed nonce null safety in transaction encoding.
* Fixed `toBigInt` handling of scientific notation and negative numbers.
* Fixed floating-point overflow in multi-leg MM pricing calculations.
* Added support for additional underlying assets and collateral tokens in the RFQ builder.

### v0.1.3 and earlier

* Initial public release of the Thetanuts Finance SDK.
* Core modules: `optionBook`, `optionFactory`, `option`, `mmPricing`, `erc20`, `api`, `utils`.
* `buildRFQParams()` and `buildRFQRequest()` high-level builders.
* `getFullOptionInfo()` aggregated option query.
* `strikeToChain()` / `strikeFromChain()` precision-safe strike conversion.
* MM pricing filter utilities: `filterExpired()`, `filterByType()`, `filterByExpiry()`, `filterByStrikeRange()`, `sortByExpiryAndStrike()`.
* Book position PnL fields added to `Position` type.
* Indexer method renames to clarify data source (`getUserPositionsFromIndexer()`, `getUserRFQsFromRfq()`, etc.).

***

## See Also

* [Migration Guide](/sdk/resources/migration-guide) - Breaking changes and upgrade steps
* [Examples](/sdk/resources/examples) - Runnable code examples
* [Quick Start](/sdk/getting-started/quick-start) - Installation and setup


