Skip to main content
Exponent offers two markets for trading PT and YT: the CLMM (Concentrated Liquidity Market Maker) for instant swaps, and the Orderbook for limit orders at specific APY levels. This page covers both, with production-ready SDK examples.

CLMM Trading

The CLMM enables instant swaps between PT and SY using concentrated liquidity pools.

Sell PT on the CLMM (go long yield)

Selling PT at a discount gives leveraged yield exposure through YT at low cost. If PT trades at 0.95, the net cost for 1,000 YT is ~50 SY.
import { Vault, MarketThree, LOCAL_ENV } from "@exponent-labs/exponent-sdk";
import { Connection, PublicKey, Transaction, sendAndConfirmTransaction, Keypair } from "@solana/web3.js";

const connection = new Connection("https://api.mainnet-beta.solana.com");
const wallet = Keypair.fromSecretKey(/* ... */);

const vault = await Vault.load(LOCAL_ENV, connection, new PublicKey("..."));
const market = await MarketThree.load(LOCAL_ENV, connection, new PublicKey("..."));

const { ixs, setupIxs } = market.ixSellPt({
  trader: wallet.publicKey,
  amountPt: 1_000_000_000n,     // 1,000 PT
  outConstraint: 940_000_000n,  // minimum 940 SY out
});

const tx = new Transaction().add(...setupIxs, ...ixs);
await sendAndConfirmTransaction(connection, tx, [wallet]);

CLMM Liquidity Provision

Providing concentrated liquidity to the CLMM earns trading fees. This example shows how to deposit liquidity, monitor a position, collect fees, and withdraw.
async function clmmLiquidityProvision() {
  const user = wallet.publicKey;

  console.log("=== CLMM Liquidity Provision ===");

  // -------------------------------------------------------------------
  // Step 1: Prepare assets (PT + SY)
  // -------------------------------------------------------------------

  console.log("Step 1: Preparing assets for liquidity provision...");

  // Strip to get PT
  const stripIx = vault.ixStrip({
    depositor: user,
    syIn: BigInt(10_000_000000), // 10,000 SY
  });

  let tx = new Transaction().add(stripIx);
  let sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

  console.log(`Stripped 10,000 SY. Received 10,000 PT + 10,000 YT`);

  // Sell some YT to get SY back for LP pairing
  await market.reload();

  const { ixs: sellYtIxs, setupIxs: sellYtSetupIxs } = market.ixSellYt({
    trader: user,
    ytIn: BigInt(5_000_000000), // Sell 5000 YT
    minSyOut: BigInt(100_000000), // Minimum 100 SY
  });

  tx = new Transaction().add(...sellYtSetupIxs, ...sellYtIxs);
  sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

  console.log(`Sold 5000 YT for ~X SY`);

  // -------------------------------------------------------------------
  // Step 2: Determine tick range for concentrated liquidity
  // -------------------------------------------------------------------

  console.log("\nStep 2: Calculating optimal tick range...");

  await market.reload();

  const currentTick = market.state.currentTick;
  const tickSpacing = market.state.tickSpacing;

  console.log(`  Current Tick: ${currentTick}`);
  console.log(`  Tick Spacing: ${tickSpacing}`);

  // Concentrated range: +/-10% around current price
  // Tick formula: tick = log_1.0001(price)
  // For +/-10%: delta tick is approximately +/-1000

  const lowerTick = Math.floor((currentTick - 1000) / tickSpacing) * tickSpacing;
  const upperTick = Math.floor((currentTick + 1000) / tickSpacing) * tickSpacing;

  console.log(`  Range: [${lowerTick}, ${upperTick}]`);

  // -------------------------------------------------------------------
  // Step 3: Deposit liquidity
  // -------------------------------------------------------------------

  console.log("\nStep 3: Depositing liquidity to CLMM...");

  const depositResult = market.ixDepositLiquidity({
    depositor: user,
    ptInIntent: BigInt(5_000_000000), // Intent to deposit 5000 PT
    syInIntent: BigInt(5_000_000000), // Intent to deposit 5000 SY
    lowerTickKey: lowerTick,
    upperTickKey: upperTick,
  });

  tx = new Transaction().add(depositResult.ix);
  sig = await sendAndConfirmTransaction(
    connection,
    tx,
    [wallet, depositResult.signers] // signers is the generated LP position keypair
  );

  const lpPositionPubkey = depositResult.signers.publicKey;

  console.log(`Deposited liquidity. Signature: ${sig}`);
  console.log(`  LP Position: ${lpPositionPubkey.toString()}`);
  console.log(`  Range: ${lowerTick} to ${upperTick}`);

  // -------------------------------------------------------------------
  // Step 4: Monitor position
  // -------------------------------------------------------------------

  console.log("\nStep 4: Monitoring LP position...");

  await market.reload();

  const { lpPositions } = await market.getUserLpPositions(user, market.selfAddress);

  for (const positions of lpPositions) {
    for (const pos of positions) {
      console.log(`  Position: ${pos.publicKey.toBase58()}`);
      console.log(`  LP Balance: ${pos.account.lpBalance}`);
      console.log(`  Lower Tick: ${pos.account.lowerTickIdx}`);
      console.log(`  Upper Tick: ${pos.account.upperTickIdx}`);

      // Preview withdrawal amounts
      const result = market.getPtAndSyOnWithdrawLiquidity(pos.account);
      console.log(`  PT on withdraw: ${result.totalPtOut}`);
      console.log(`  SY on withdraw: ${result.totalSyOut}`);
    }
  }

  // -------------------------------------------------------------------
  // Step 5: Collect fees
  // -------------------------------------------------------------------

  console.log("\nStep 5: Collecting fees...");

  // Fees are collected automatically when withdrawing liquidity
  // Or can be collected separately via ixAddLiquidity with 0 amounts

  const { ixs: collectIxs, setupIxs: collectSetupIxs } = await market.ixAddLiquidity({
    owner: user,
    lpPosition: lpPositionPubkey,
    ptInIntent: 0n,
    syInIntent: 0n,
  });

  tx = new Transaction().add(...collectSetupIxs, ...collectIxs);
  sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

  console.log(`Collected fees. Signature: ${sig}`);

  // -------------------------------------------------------------------
  // Step 6: Withdraw liquidity
  // -------------------------------------------------------------------

  console.log("\nStep 6: Withdrawing liquidity...");

  const withdrawLiquidityIx = market.ixWithdrawLiquidity({
    withdrawer: user,
    lpPosition: lpPositionPubkey,
    lpIn: BigInt(5_000_000000), // LP tokens to burn
    minPtOut: 0n, // Set appropriate slippage limits in production
    minSyOut: 0n,
  });

  tx = new Transaction().add(withdrawLiquidityIx);
  sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

  console.log(`Withdrew liquidity. Signature: ${sig}`);
  console.log(`  Received: PT + SY + accrued fees`);

  console.log("\n=== CLMM Liquidity Provision Complete ===");
}
Key Takeaways:
  • Concentrated liquidity requires choosing a tick range
  • Tighter ranges mean higher capital efficiency but more rebalancing
  • Fees accrue automatically when trades occur within the range
  • Use ixAddLiquidity with 0 amounts to collect fees without depositing
  • LP positions are NFT-like accounts (unique per range)

Orderbook Trading

The Orderbook allows limit orders at specific APY levels, giving precise control over pricing. YT sitting in open SellYT offers continues to earn yield.

Post a YT sell offer (lock in a fixed rate)

Selling YT leaves the trader with only PT — a fixed-rate position that converges to its SY backing at maturity.
import { Orderbook, OfferType, offerOptions } from "@exponent-labs/exponent-sdk";

const orderbook = await Orderbook.load(LOCAL_ENV, connection, new PublicKey("..."));

const postOfferIx = orderbook.ixPostOffer({
  trader: wallet.publicKey,
  price: 0.12,
  amount: 1_000_000_000n,
  offerType: OfferType.SellYt,
  offerOption: offerOptions("FillOrKill", [false]),
  virtualOffer: false,
  expirySeconds: 86400 * 7,
  mintSy: vault.mintSy,
});

const tx = new Transaction().add(postOfferIx);
await sendAndConfirmTransaction(connection, tx, [wallet]);

Orderbook Limit Order with Interest Collection

This example posts a YT limit order and collects yield that accrues while the order sits in the book.
import { OfferType, offerOptions, amount } from "@exponent-labs/exponent-sdk";

async function orderbookLimitOrderWithInterest() {
  const user = wallet.publicKey;

  console.log("=== Orderbook Limit Order + Interest ===");

  // -------------------------------------------------------------------
  // Step 1: Strip SY into PT + YT
  // -------------------------------------------------------------------

  console.log("Step 1: Stripping 2000 SY into PT + YT...");

  const stripIx = vault.ixStrip({
    depositor: user,
    syIn: BigInt(2000_000000),
  });

  let tx = new Transaction().add(stripIx);
  let sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

  console.log(`Stripped SY. Signature: ${sig}`);
  console.log(`  Received: 2000 PT + 2000 YT`);

  // -------------------------------------------------------------------
  // Step 2: Post limit order to sell 1500 YT at 15% implied APY
  // -------------------------------------------------------------------

  console.log("\nStep 2: Posting SellYT limit order at 15% APY...");

  await orderbook.reload();

  const postOfferIx = orderbook.ixPostOffer({
    trader: user,
    price: 0.15, // 15% implied APY
    amount: 1_500_000000n, // 1500 YT
    offerType: OfferType.SellYt,
    offerOption: offerOptions("FillOrKill", [false]), // Allow partial fills
    virtualOffer: false, // Real YT offer (not PT)
    expirySeconds: 86400 * 7, // 7 day expiry
    mintSy: vault.mintSy,
  });

  tx = new Transaction().add(postOfferIx);
  sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

  console.log(`Posted limit order. Signature: ${sig}`);
  console.log(`  Order: Sell 1500 YT at 15% APY`);
  console.log(`  Expiry: 7 days`);

  // -------------------------------------------------------------------
  // Step 3: Wait for order to fill OR time to pass
  // -------------------------------------------------------------------

  console.log("\nStep 3: Waiting for order to fill or yield to accrue...");
  console.log("  (Order sits in book, YT earns yield)");

  // In production: monitor order status
  // const openOrders = await orderbook.getOpenOrders({ user });
  // Check if order is filled, partially filled, or still open

  // Simulate time passing (in reality this would be days)
  await new Promise(resolve => setTimeout(resolve, 5000));

  // -------------------------------------------------------------------
  // Step 4: Collect interest on YT in open order
  // -------------------------------------------------------------------

  console.log("\nStep 4: Collecting interest on YT in order book...");

  await orderbook.reload();

  // Orderbook wrapper instruction handles SY conversion
  const collectInterestIx = await orderbook.ixWrapperCollectInterest({
    trader: user,
    mintSy: vault.mintSy,
  });

  tx = new Transaction().add(collectInterestIx);
  sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

  console.log(`Collected interest. Signature: ${sig}`);
  console.log(`  Received: ~X SY (yield accrued on YT in open order)`);

  // -------------------------------------------------------------------
  // Step 5: Check order status and balances
  // -------------------------------------------------------------------

  console.log("\nStep 5: Checking order status and escrow balances...");

  await orderbook.reload();

  const userBalances = orderbook.getUserBalances(user);

  console.log(`User Escrow Balances:`);
  console.log(`  PT: ${userBalances.pt}`);
  console.log(`  YT: ${userBalances.yt}`);
  console.log(`  SY: ${userBalances.sy}`);
  console.log(`  Staked YT: ${userBalances.stakedYt}`);
  console.log(`  Staged SY: ${userBalances.staged}`);

  const openOrders = orderbook.getUserOpenOrders(user);
  console.log(`\nOpen Orders: ${openOrders.length}`);

  openOrders.forEach((order, idx) => {
    console.log(`  Order ${idx + 1}:`);
    console.log(`    Type: ${order.type}`);
    console.log(`    Amount: ${order.amount}`);
    console.log(`    Price APY: ${order.priceApy}%`);
    console.log(`    Virtual: ${order.isVirtual}`);
    console.log(`    Expiry: ${new Date(order.expiryAt * 1000).toISOString()}`);
  });

  // -------------------------------------------------------------------
  // Step 6: Withdraw escrow balances
  // -------------------------------------------------------------------

  console.log("\nStep 6: Withdrawing escrow balances...");

  const withdrawIx = await orderbook.ixWrapperWithdrawFunds({
    trader: user,
    mintSy: vault.mintSy,
    ptAmount: amount("All"),
    ytAmount: amount("All"),
    syAmount: amount("All"),
  });

  tx = new Transaction().add(withdrawIx);
  sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

  console.log(`Withdrew escrow balances. Signature: ${sig}`);

  // -------------------------------------------------------------------
  // Step 7 (Optional): Remove unfilled order
  // -------------------------------------------------------------------

  if (openOrders.length > 0) {
    console.log("\nStep 7: Removing unfilled order...");

    const removeOfferIx = await orderbook.ixWrapperRemoveOffer({
      trader: user,
      offerIdx: openOrders[0].offerIndex, // Remove first order
      mintSy: vault.mintSy,
    });

    tx = new Transaction().add(removeOfferIx);
    sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

    console.log(`Removed order. Signature: ${sig}`);
  }

  console.log("\n=== Orderbook Limit Order + Interest Complete ===");
}
Key Takeaways:
  • YT in orderbook SellYT offers continues to earn yield
  • Use ixWrapperCollectInterest to claim yield from the orderbook
  • getUserBalances shows escrow balances separate from wallet balances
  • getUserOpenOrders returns all active orders for a user
  • Always withdraw escrow balances or remove orders to recover funds

Virtual Offers: Trade PT Directly on the Orderbook

Virtual offers enable PT trading on the Orderbook without handling YT directly. The system automatically strips or merges to settle virtual trades.
async function virtualOffers() {
  const user = wallet.publicKey;

  console.log("=== Virtual Offers (PT Trading) ===");

  // -------------------------------------------------------------------
  // Step 1: Post virtual SellYT offer (actually selling PT for SY)
  // -------------------------------------------------------------------

  console.log("Step 1: Posting virtual SellYT offer (sell PT for SY)...");

  await vault.reload();
  await orderbook.reload();

  // Virtual SellYT:
  // - User deposits SY
  // - When matched, system strips SY into PT + YT
  // - User receives PT, counter-party receives YT

  const baseAmount = 1000_000000; // 1000 USDC worth

  const { ix: virtualSellIx, setupIxs: virtualSellSetupIxs } = await orderbook.ixWrapperPostOffer({
    trader: user,
    amount: BigInt(baseAmount),
    price: 0.12, // 12% implied APY
    offerType: OfferType.SellYt,
    offerOption: offerOptions("FillOrKill", [false]),
    virtualOffer: true, // VIRTUAL = selling PT
    expirySeconds: 86400 * 3, // 3 day expiry
    mintSy: vault.mintSy,
  });

  let tx = new Transaction().add(...virtualSellSetupIxs, virtualSellIx);
  let sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

  console.log(`Posted virtual SellYT offer. Signature: ${sig}`);
  console.log(`  Deposited: 1000 USDC worth of SY`);
  console.log(`  When filled: Receive PT, counter-party gets YT`);

  // -------------------------------------------------------------------
  // Step 2: Post virtual BuyYT offer (actually buying SY with PT)
  // -------------------------------------------------------------------

  console.log("\nStep 2: Posting virtual BuyYT offer (buy SY with PT)...");

  // First, get some PT (strip SY)
  const stripIx = vault.ixStrip({
    depositor: user,
    syIn: BigInt(500_000000),
  });

  tx = new Transaction().add(stripIx);
  sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

  console.log(`Stripped 500 SY. Now have PT to trade.`);

  await vault.reload();

  // Virtual BuyYT:
  // - User deposits PT
  // - When matched, system merges PT + YT into SY
  // - User receives SY, counter-party provides YT

  const virtualBuyIx = orderbook.ixPostOffer({
    trader: user,
    price: 0.10, // 10% implied APY
    amount: 500_000000n, // 500 PT
    offerType: OfferType.BuyYt,
    offerOption: offerOptions("FillOrKill", [false]),
    virtualOffer: true, // VIRTUAL = buying SY
    expirySeconds: 86400 * 3,
    mintSy: vault.mintSy,
  });

  tx = new Transaction().add(virtualBuyIx);
  sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

  console.log(`Posted virtual BuyYT offer. Signature: ${sig}`);
  console.log(`  Deposited: 500 PT`);
  console.log(`  When filled: Receive SY`);

  // -------------------------------------------------------------------
  // Step 3: Market order to fill virtual offer
  // -------------------------------------------------------------------

  console.log("\nStep 3: Executing market order to fill virtual offer...");

  // Take the virtual SellYT offer (receive PT)
  const { ix: marketOfferIx, setupIxs: marketSetupIxs } = await orderbook.ixWrapperMarketOffer({
    trader: user,
    amount: BigInt(300_000000), // Buy 300 USDC worth
    maxPriceApy: 0.13, // Max 13% APY
    minAmountOut: BigInt(290_000000), // Min output
    offerType: OfferType.BuyYt, // Buying YT (taking SellYT offers)
    virtualOffer: false, // This side is not virtual
    mintSy: vault.mintSy,
  });

  tx = new Transaction().add(...marketSetupIxs, marketOfferIx);
  sig = await sendAndConfirmTransaction(connection, tx, [wallet]);

  console.log(`Market order executed. Signature: ${sig}`);
  console.log(`  Filled virtual SellYT offers`);
  console.log(`  Received: YT tokens (from stripped SY)`);

  // -------------------------------------------------------------------
  // Step 4: Check results
  // -------------------------------------------------------------------

  console.log("\nStep 4: Checking balances and open orders...");

  await orderbook.reload();

  const balances = orderbook.getUserBalances(user);
  const openOrders = orderbook.getUserOpenOrders(user);

  console.log(`Escrow Balances:`);
  console.log(`  PT: ${balances.pt}`);
  console.log(`  YT: ${balances.yt}`);
  console.log(`  SY: ${balances.sy}`);

  console.log(`\nOpen Virtual Offers: ${openOrders.filter(o => o.isVirtual).length}`);

  console.log("\n=== Virtual Offers Complete ===");
}
Key Takeaways:
  • Virtual SellYT = Sell PT for SY (deposit SY, receive PT when filled)
  • Virtual BuyYT = Buy SY with PT (deposit PT, receive SY when filled)
  • Virtual offers enable PT traders to avoid YT entirely
  • The system automatically strips/merges to settle virtual trades
  • Virtual and non-virtual offers share the same orderbook liquidity

Next Steps