🕐 Time to complete: ±10 minutes

🧰 What you need:

Introduction

Pendle lets you tokenize and trade future yield. With Compass Labs, you can interact with Pendle markets - including buying/selling PT, YT, and LP positions - in just a few lines of code.

Normally this would require complex smart contract logic and allowance juggling. With Compass SDK, you abstract all of that away. This tutorial walks you through a full strategy lifecycle across fixed and variable yield.

Setup

1

Install Dependencies

Install the required packages.

npm install @compass-labs/api-sdk viem dotenv
2

Set Environment Variables

Create a .env file in your project root.

.env
PRIVATE_KEY="your_wallet_private_key"
RPC_URL="your_ethereum_rpc_url"
COMPASS_API_KEY="your_compass_api_key"
3

Import Libraries & Environment Variables

import { CompassApiSDK } from "@compass-labs/api-sdk";
import dotenv from "dotenv";
import { privateKeyToAccount } from "viem/accounts";
import { arbitrum } from "viem/chains";
import { http, createWalletClient, createPublicClient } from "viem";
import { Contract } from "@compass-labs/api-sdk/models/operations";

dotenv.config();

const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;
const RPC_URL = process.env.RPC_URL as string;
const account = privateKeyToAccount(PRIVATE_KEY);
const WALLET_ADDRESS = account.address;
4

Instantiate Required Clients

const compassApiSDK = new CompassApiSDK({
  apiKeyAuth: process.env.COMPASS_API_KEY,
});

const walletClient = createWalletClient({
  account,
  chain: arbitrum,
  transport: http(RPC_URL),
});

const publicClient = createPublicClient({
  chain: arbitrum,
  transport: http(RPC_URL),
});

The full, uninterrupted code is available at the end of the tutorial.

Query Pendle Markets

First, we need a Pendle market to perform transactions with. Let’s query the list of active markets and select one we’d like to interact with.

1

Query Active Markets

Use the markets function to query a list of active market on Arbitrum.

const { markets } = await compassApiSDK.pendle.markets({
  chain: "arbitrum:mainnet",
});
2

Select a Market

For simplicity, we are selecting the first market returned of the list of active markets with markets[0]. Consider things like implied yield, maturity date and the underlying token when deciding which Pendle market to transact with.

const market = markets[0];
3

Destructure Variables

Destructure variables from the selected market that will be used ultiple times throughout the rest of the tutorial.

const marketAddress = market.address;
const underlyingAssetAddress = market.underlyingAsset.split("-")[1];
const ptAddress = market.pt.split("-")[1];
const ytAddress = market.yt.split("-")[1];

Here you can see we are also checking our current position in the Pendle market using the position function. We will use this function a number of times throughout the tutorial to keep track of our position balances in the market.

Fixed Yield

Buy Principal Token (PT) to Earn Fixed Yield

Now that we have decided on a Pendle market to transact with, let us take our first position in the market by buying fixed yield. Fixed yield positions are represented by PT tokens. The price difference between 1 PT and 1 Underlying Asset represents the fixed yield your entitled to claim after the maturity date of the market when PT tokens can be redeemed 1:1 for the Underlying Asset.

1

Check User Position

Check our current position in the Pendle market using the position function.

let userPosition = await compassApiSDK.pendle.position({
  chain: "arbitrum:mainnet",
  userAddress: WALLET_ADDRESS,
  marketAddress,
});
2

Check and Set Allowance

Check the current allowance given for the Pendle Router on the Underlying Asset contract. If that allowance is less than the amount of Underlying Asset we intend to use to purchase PT, then set the allowance to a sufficient level.

let underlyingAssetAllowance = await compassApiSDK.universal.allowance({
  chain: "arbitrum:mainnet",
  user: WALLET_ADDRESS,
  token: underlyingAssetAddress,
  contract: Contract.PendleRouter,
});

if (underlyingAssetAllowance.amount < userPosition.underlyingTokenBalance) {
  // Set new allowance if current underlying asset allowance for Pendle Router is insufficient
  const setAllowanceForUnderlyingAssetTx =
    await compassApiSDK.universal.allowanceSet({
      chain: "arbitrum:mainnet",
      sender: WALLET_ADDRESS,
      token: underlyingAssetAddress,
      contract: Contract.PendleRouter,
      amount: userPosition.underlyingTokenBalance,
    });

  const txHash = await walletClient.sendTransaction(
    setAllowanceForUnderlyingAssetTx as any
  );

  await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
}
3

Buy PT

Buy PT with the market’s Underlying Asset and await confirmation that the transaction has been included on a block.

const buyPtTx = await compassApiSDK.pendle.buyPt({
  chain: "arbitrum:mainnet",
  sender: WALLET_ADDRESS,
  marketAddress,
  amount: userPosition.underlyingTokenBalance,
  maxSlippagePercent: 0.1,
});

let txHash = await walletClient.sendTransaction(buyPtTx as any);

await publicClient.waitForTransactionReceipt({
  hash: txHash,
});

Sell Principal Token (PT) Before Maturity Date

Let’s say some time after we purchsed our PT, we notice the market offering a more favorable signal for going long on yield. This likely means the implied yield rate has come down since we first took the PT position. Before going long on yield, we may want to sell our PT position beforehand and use the proceeds to fund our YT position.

1

Check User Position

Check our current position in the Pendle market using the position function.

userPosition = await compassApiSDK.pendle.position({
  chain: "arbitrum:mainnet",
  userAddress: WALLET_ADDRESS,
  marketAddress,
});
2

Check and Set Allowance

Check the current allowance given for the Pendle Router on the PT contract. If that allowance is less than the amount of PT we intend to sell, then set the allowance to a sufficient level.

const pTAllowance = await compassApiSDK.universal.allowance({
  chain: "arbitrum:mainnet",
  user: WALLET_ADDRESS,
  token: ptAddress,
  contract: Contract.PendleRouter,
});

if (pTAllowance.amount < userPosition.ptBalance) {
  // Set new allowance if current PT allowance for Pendle Router is insufficient
  const setAllowanceForPtTx = await compassApiSDK.universal.allowanceSet({
    chain: "arbitrum:mainnet",
    sender: WALLET_ADDRESS,
    token: ptAddress,
    contract: Contract.PendleRouter,
    amount: userPosition.ptBalance,
  });

  const txHash = await walletClient.sendTransaction(setAllowanceForPtTx as any);

  await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
}
3

Sell PT

Sell PT for the market’s Underlying Asset and await confirmation that the transaction has been included on a block.

const sellPtTx = await compassApiSDK.pendle.sellPt({
  chain: "arbitrum:mainnet",
  sender: WALLET_ADDRESS,
  marketAddress,
  amount: userPosition.ptBalance,
  maxSlippagePercent: 0.1,
});

txHash = await walletClient.sendTransaction(sellPtTx as any);

await publicClient.waitForTransactionReceipt({
  hash: txHash,
});

Both Principal Tokens (PT) and Yield Tokens (YT) are ERC-20 compatible and, just like the Underlying Asset, require sufficient spending allowances to be set on them before a given contract can spend a user’s balance.

Variable Yield

Buy Yield Token (YT) to Earn Variable Yield

We have decided a long yield position is now favorable. This is likely because the implied yield rate has lowered. We now think that the yield generated from the Underlying Asset will be greater than the current implied yield from now until the market’s maturity date.

1

Check User Position

Check our current position in the Pendle market using the position function.

userPosition = await compassApiSDK.pendle.position({
  chain: "arbitrum:mainnet",
  userAddress: WALLET_ADDRESS,
  marketAddress,
});
2

Check and Set Allowance

Check the current allowance given for the Pendle Router on the Underlying Asset contract. If that allowance is less than the amount of Underlying Asset we intend to use to purchase YT, then set the allowance to a sufficient level.

underlyingAssetAllowance = await compassApiSDK.universal.allowance({
  chain: "arbitrum:mainnet",
  user: WALLET_ADDRESS,
  token: underlyingAssetAddress,
  contract: Contract.PendleRouter,
});

if (underlyingAssetAllowance.amount < userPosition.underlyingTokenBalance) {
  // Set new allowance if current underlying asset allowance for Pendle Router is insufficient
  const setAllowanceForUnderlyingAssetTx =
    await compassApiSDK.universal.allowanceSet({
      chain: "arbitrum:mainnet",
      sender: WALLET_ADDRESS,
      token: underlyingAssetAddress,
      contract: Contract.PendleRouter,
      amount: userPosition.underlyingTokenBalance,
    });

  const txHash = await walletClient.sendTransaction(
    setAllowanceForUnderlyingAssetTx as any
  );

  await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
}
3

Buy YT

Buy YT with the market’s Underlying Asset and await confirmation that the transaction has been included on a block.

const buyYtTx = await compassApiSDK.pendle.buyYt({
  chain: "arbitrum:mainnet",
  sender: WALLET_ADDRESS,
  marketAddress,
  amount: userPosition.underlyingTokenBalance,
  maxSlippagePercent: 0.1,
});

txHash = await walletClient.sendTransaction(buyYtTx as any);

await publicClient.waitForTransactionReceipt({
  hash: txHash,
});

Redeem Claimable Yield and Sell YT Position

Perhaps now the implied yield has increased again and we’d like to sell our Yield Tokens (YT) for a profit before the maturity date of the market.

1

Redeem Claimable Yield

Redeem any unstanding claimable yield on our YT.

const redeemYieldTx = await compassApiSDK.pendle.redeemYield({
  chain: "arbitrum:mainnet",
  sender: WALLET_ADDRESS,
  marketAddress,
});

txHash = await walletClient.sendTransaction(redeemYieldTx as any);

await publicClient.waitForTransactionReceipt({
  hash: txHash,
});
2

Check User Position

Check our current position in the Pendle market using the position function.

userPosition = await compassApiSDK.pendle.position({
  chain: "arbitrum:mainnet",
  userAddress: WALLET_ADDRESS,
  marketAddress,
});
3

Check and Set Allowance

Check the current allowance given for the Pendle Router on the YT contract. If that allowance is less than the amount of YT we intend to sell, then set the allowance to a sufficient level.

const yTAllowance = await compassApiSDK.universal.allowance({
  chain: "arbitrum:mainnet",
  user: WALLET_ADDRESS,
  token: ytAddress,
  contract: Contract.PendleRouter,
});

if (yTAllowance.amount < userPosition.ytBalance) {
  // Set new allowance if current YT allowance for Pendle Router is insufficient
  const setAllowanceForPtTx = await compassApiSDK.universal.allowanceSet({
    chain: "arbitrum:mainnet",
    sender: WALLET_ADDRESS,
    token: ytAddress,
    contract: Contract.PendleRouter,
    amount: userPosition.ytBalance,
  });

  const txHash = await walletClient.sendTransaction(setAllowanceForPtTx as any);

  await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
}
4

Sell YT

Sell YT for the market’s Underlying Asset and await confirmation that the transaction has been included on a block.

const sellYtTx = await compassApiSDK.pendle.sellYt({
  chain: "arbitrum:mainnet",
  sender: WALLET_ADDRESS,
  marketAddress,
  amount: userPosition.ytBalance,
  maxSlippagePercent: 0.1,
});

txHash = await walletClient.sendTransaction(sellYtTx as any);

await publicClient.waitForTransactionReceipt({
  hash: txHash,
});

It’s best practice to always redeem claimable yield before selling any of your YT balance. Once the YT is sold, you relinquish ownership and thus lose the ability to claim any yield, as the yield rights are tied to the token itself, not your past ownership.

Provide Liquidity

Add Liquidity (LP)

Let’s say we are uncertain whether to take fixed yield or go long yield in a particular Pendle market but we’d still like to earn yield. Another option is to provide liquidity to the market and earn yield in the form of fees accrued by the market from other users taking positions. The liquidity you provide to a given Pendle market is represented by LP tokens.

1

Check User Position

Check our current position in the Pendle market using the position function.

userPosition = await compassApiSDK.pendle.position({
  chain: "arbitrum:mainnet",
  userAddress: WALLET_ADDRESS,
  marketAddress,
});
2

Check and Set Allowance

Check the current allowance given for the Pendle Router on the Underlying Asset contract. If that allowance is less than the amount of Underlying Asset we intend to use to purchase LP, then set the allowance to a sufficient level.

underlyingAssetAllowance = await compassApiSDK.universal.allowance({
  chain: "arbitrum:mainnet",
  user: WALLET_ADDRESS,
  token: underlyingAssetAddress,
  contract: Contract.PendleRouter,
});

if (underlyingAssetAllowance.amount < userPosition.underlyingTokenBalance) {
  // Set new allowance if current underlying asset allowance for Pendle Router is insufficient
  const setAllowanceForUnderlyingAssetTx =
    await compassApiSDK.universal.allowanceSet({
      chain: "arbitrum:mainnet",
      sender: WALLET_ADDRESS,
      token: underlyingAssetAddress,
      contract: Contract.PendleRouter,
      amount: userPosition.underlyingTokenBalance,
    });

  const txHash = await walletClient.sendTransaction(
    setAllowanceForUnderlyingAssetTx as any
  );

  await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
}
3

Add Liquidity

Add liquidity with the market’s Underlying Asset in exchange for LP and await confirmation that the transaction has been included on a block.

const addLiquidityTx = await compassApiSDK.pendle.addLiquidity({
  chain: "arbitrum:mainnet",
  sender: WALLET_ADDRESS,
  marketAddress,
  amount: userPosition.underlyingTokenBalance,
  maxSlippagePercent: 0.1,
});

txHash = await walletClient.sendTransaction(addLiquidityTx as any);

await publicClient.waitForTransactionReceipt({
  hash: txHash,
});

For further practice, try removing liquidity from the market using the removeLiquidity function, test with different Pendle markets, or explore Compass Bundler to bundle transactions.

Full Code

Here is the full script from the tutorial. Copy and paste into your code editor and play around!

import { CompassApiSDK } from "@compass-labs/api-sdk";
import dotenv from "dotenv";
import { privateKeyToAccount } from "viem/accounts";
import { arbitrum } from "viem/chains";
import { http, createWalletClient, createPublicClient } from "viem";
import { Contract } from "@compass-labs/api-sdk/models/operations";

dotenv.config();

const PRIVATE_KEY = process.env.PRIVATE_KEY as `0x${string}`;
const RPC_URL = process.env.RPC_URL as string;
const account = privateKeyToAccount(PRIVATE_KEY);
const WALLET_ADDRESS = account.address;

const compassApiSDK = new CompassApiSDK({
  apiKeyAuth: process.env.COMPASS_API_KEY,
});

const walletClient = createWalletClient({
  account,
  chain: arbitrum,
  transport: http(RPC_URL),
});

const publicClient = createPublicClient({
  chain: arbitrum,
  transport: http(RPC_URL),
});

const { markets } = await compassApiSDK.pendle.markets({
  chain: "arbitrum:mainnet",
});

const market = markets[0];

const marketAddress = market.address;
const underlyingAssetAddress = market.underlyingAsset.split("-")[1];
const ptAddress = market.pt.split("-")[1];
const ytAddress = market.yt.split("-")[1];

let userPosition = await compassApiSDK.pendle.position({
  chain: "arbitrum:mainnet",
  userAddress: WALLET_ADDRESS,
  marketAddress,
});

let underlyingAssetAllowance = await compassApiSDK.universal.allowance({
  chain: "arbitrum:mainnet",
  user: WALLET_ADDRESS,
  token: underlyingAssetAddress,
  contract: Contract.PendleRouter,
});

if (underlyingAssetAllowance.amount < userPosition.underlyingTokenBalance) {
  // Set new allowance if current underlying asset allowance for Pendle Router is insufficient
  const setAllowanceForUnderlyingAssetTx =
    await compassApiSDK.universal.allowanceSet({
      chain: "arbitrum:mainnet",
      sender: WALLET_ADDRESS,
      token: underlyingAssetAddress,
      contract: Contract.PendleRouter,
      amount: userPosition.underlyingTokenBalance,
    });

  const txHash = await walletClient.sendTransaction(
    setAllowanceForUnderlyingAssetTx as any
  );

  await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
}

const buyPtTx = await compassApiSDK.pendle.buyPt({
  chain: "arbitrum:mainnet",
  sender: WALLET_ADDRESS,
  marketAddress,
  amount: userPosition.underlyingTokenBalance,
  maxSlippagePercent: 0.1,
});

let txHash = await walletClient.sendTransaction(buyPtTx as any);

await publicClient.waitForTransactionReceipt({
  hash: txHash,
});

userPosition = await compassApiSDK.pendle.position({
  chain: "arbitrum:mainnet",
  userAddress: WALLET_ADDRESS,
  marketAddress,
});

const pTAllowance = await compassApiSDK.universal.allowance({
  chain: "arbitrum:mainnet",
  user: WALLET_ADDRESS,
  token: ptAddress,
  contract: Contract.PendleRouter,
});

if (pTAllowance.amount < userPosition.ptBalance) {
  // Set new allowance if current PT allowance for Pendle Router is insufficient
  const setAllowanceForPtTx = await compassApiSDK.universal.allowanceSet({
    chain: "arbitrum:mainnet",
    sender: WALLET_ADDRESS,
    token: ptAddress,
    contract: Contract.PendleRouter,
    amount: userPosition.ptBalance,
  });

  const txHash = await walletClient.sendTransaction(setAllowanceForPtTx as any);

  await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
}

const sellPtTx = await compassApiSDK.pendle.sellPt({
  chain: "arbitrum:mainnet",
  sender: WALLET_ADDRESS,
  marketAddress,
  amount: userPosition.ptBalance,
  maxSlippagePercent: 0.1,
});

txHash = await walletClient.sendTransaction(sellPtTx as any);

await publicClient.waitForTransactionReceipt({
  hash: txHash,
});

userPosition = await compassApiSDK.pendle.position({
  chain: "arbitrum:mainnet",
  userAddress: WALLET_ADDRESS,
  marketAddress,
});

underlyingAssetAllowance = await compassApiSDK.universal.allowance({
  chain: "arbitrum:mainnet",
  user: WALLET_ADDRESS,
  token: underlyingAssetAddress,
  contract: Contract.PendleRouter,
});

if (underlyingAssetAllowance.amount < userPosition.underlyingTokenBalance) {
  // Set new allowance if current underlying asset allowance for Pendle Router is insufficient
  const setAllowanceForUnderlyingAssetTx =
    await compassApiSDK.universal.allowanceSet({
      chain: "arbitrum:mainnet",
      sender: WALLET_ADDRESS,
      token: underlyingAssetAddress,
      contract: Contract.PendleRouter,
      amount: userPosition.underlyingTokenBalance,
    });

  const txHash = await walletClient.sendTransaction(
    setAllowanceForUnderlyingAssetTx as any
  );

  await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
}

const buyYtTx = await compassApiSDK.pendle.buyYt({
  chain: "arbitrum:mainnet",
  sender: WALLET_ADDRESS,
  marketAddress,
  amount: userPosition.underlyingTokenBalance,
  maxSlippagePercent: 0.1,
});

txHash = await walletClient.sendTransaction(buyYtTx as any);

await publicClient.waitForTransactionReceipt({
  hash: txHash,
});

const redeemYieldTx = await compassApiSDK.pendle.redeemYield({
  chain: "arbitrum:mainnet",
  sender: WALLET_ADDRESS,
  marketAddress,
});

txHash = await walletClient.sendTransaction(redeemYieldTx as any);

await publicClient.waitForTransactionReceipt({
  hash: txHash,
});

userPosition = await compassApiSDK.pendle.position({
  chain: "arbitrum:mainnet",
  userAddress: WALLET_ADDRESS,
  marketAddress,
});

const yTAllowance = await compassApiSDK.universal.allowance({
  chain: "arbitrum:mainnet",
  user: WALLET_ADDRESS,
  token: ytAddress,
  contract: Contract.PendleRouter,
});

if (yTAllowance.amount < userPosition.ytBalance) {
  // Set new allowance if current YT allowance for Pendle Router is insufficient
  const setAllowanceForPtTx = await compassApiSDK.universal.allowanceSet({
    chain: "arbitrum:mainnet",
    sender: WALLET_ADDRESS,
    token: ytAddress,
    contract: Contract.PendleRouter,
    amount: userPosition.ytBalance,
  });

  const txHash = await walletClient.sendTransaction(setAllowanceForPtTx as any);

  await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
}

const sellYtTx = await compassApiSDK.pendle.sellYt({
  chain: "arbitrum:mainnet",
  sender: WALLET_ADDRESS,
  marketAddress,
  amount: userPosition.ytBalance,
  maxSlippagePercent: 0.1,
});

txHash = await walletClient.sendTransaction(sellYtTx as any);

await publicClient.waitForTransactionReceipt({
  hash: txHash,
});

userPosition = await compassApiSDK.pendle.position({
  chain: "arbitrum:mainnet",
  userAddress: WALLET_ADDRESS,
  marketAddress,
});

underlyingAssetAllowance = await compassApiSDK.universal.allowance({
  chain: "arbitrum:mainnet",
  user: WALLET_ADDRESS,
  token: underlyingAssetAddress,
  contract: Contract.PendleRouter,
});

if (underlyingAssetAllowance.amount < userPosition.underlyingTokenBalance) {
  // Set new allowance if current underlying asset allowance for Pendle Router is insufficient
  const setAllowanceForUnderlyingAssetTx =
    await compassApiSDK.universal.allowanceSet({
      chain: "arbitrum:mainnet",
      sender: WALLET_ADDRESS,
      token: underlyingAssetAddress,
      contract: Contract.PendleRouter,
      amount: userPosition.underlyingTokenBalance,
    });

  const txHash = await walletClient.sendTransaction(
    setAllowanceForUnderlyingAssetTx as any
  );

  await publicClient.waitForTransactionReceipt({
    hash: txHash,
  });
}

const addLiquidityTx = await compassApiSDK.pendle.addLiquidity({
  chain: "arbitrum:mainnet",
  sender: WALLET_ADDRESS,
  marketAddress,
  amount: userPosition.underlyingTokenBalance,
  maxSlippagePercent: 0.1,
});

txHash = await walletClient.sendTransaction(addLiquidityTx as any);

await publicClient.waitForTransactionReceipt({
  hash: txHash,
});