NestDevelopers
Convert USDC and nUSD

Redeem nUSD for USDC

Read PSM liquidity limits and redeem nUSD for USDC with psm_swap_out.

psm_swap_out(amount) burns the user's nUSD and transfers the same raw amount of USDC from the PSM vault.

Redemption is unavailable while protocol bad debt is outstanding. It is also bounded by idle PSM USDC and the configured rolling outflow circuit breaker.

TypeScript example

nusd-to-usdc.ts
import { BN } from "@coral-xyz/anchor";
import { PublicKey, Transaction } from "@solana/web3.js";
import { getAccount } from "@solana/spl-token";
import {
  addAtaIfMissing,
  ata,
  simulateSignSend,
  toBaseUnits,
  type NestClient,
} from "../nest-client";

export async function redeemNusdForUsdc(client: NestClient, userAmount: string) {
  const owner = client.wallet.publicKey;
  const amount = toBaseUnits(userAmount, 6);
  if (amount <= 0n) throw new Error("Redemption amount must be positive");

  const protocol = new PublicKey(client.deployment.accounts.protocol);
  const usdcMint = new PublicKey(client.deployment.mints.usdc);
  const nusdMint = new PublicKey(client.deployment.mints.nusd);
  const usdcTokenProgram = new PublicKey(client.deployment.tokenPrograms.usdc);
  const nusdTokenProgram = new PublicKey(client.deployment.tokenPrograms.nusd);
  const userNusdAccount = ata(nusdMint, owner, nusdTokenProgram);
  const psmUsdcVault = new PublicKey(client.deployment.vaults.psmUsdcVault);

  const [protocolBefore, sourceBefore, psmVaultBefore] = await Promise.all([
    client.core.account.protocol.fetch(protocol),
    getAccount(client.connection, userNusdAccount, "confirmed", nusdTokenProgram),
    getAccount(client.connection, psmUsdcVault, "confirmed", usdcTokenProgram),
  ]);
  if (protocolBefore.paused) throw new Error("Nest Core is paused");
  if (BigInt(protocolBefore.psmSwapOutFeeBps.toString()) !== 0n) {
    throw new Error("This integration expects the current zero-fee PSM configuration");
  }
  if (BigInt(protocolBefore.badDebtNusd.toString()) !== 0n) {
    throw new Error("PSM redemption is disabled while bad debt is outstanding");
  }
  if (sourceBefore.amount < amount) throw new Error("Insufficient nUSD balance");
  const idleBefore = BigInt(protocolBefore.psmIdleUsdc.toString());
  if (amount > idleBefore || amount > psmVaultBefore.amount) {
    throw new Error("Insufficient idle PSM USDC");
  }

  const transaction = new Transaction();
  const userUsdcAccount = await addAtaIfMissing(
    client,
    transaction,
    usdcMint,
    owner,
    usdcTokenProgram,
  );
  const destinationBefore = await client.connection.getAccountInfo(userUsdcAccount, "confirmed")
    ? (await getAccount(
        client.connection,
        userUsdcAccount,
        "confirmed",
        usdcTokenProgram,
      )).amount
    : 0n;

  transaction.add(await client.core.methods
    .psmSwapOut(new BN(amount.toString()))
    .accountsStrict({
      protocol,
      usdcMint,
      nusdMint,
      userNusdAccount,
      psmUsdcVault,
      userUsdcAccount,
      user: owner,
      nusdTokenProgram,
      usdcTokenProgram,
    })
    .instruction());

  const signature = await simulateSignSend(client, transaction);
  const [sourceAfter, destinationAfter, protocolAfter] = await Promise.all([
    getAccount(client.connection, userNusdAccount, "confirmed", nusdTokenProgram),
    getAccount(client.connection, userUsdcAccount, "confirmed", usdcTokenProgram),
    client.core.account.protocol.fetch(protocol),
  ]);
  if (
    sourceAfter.amount !== sourceBefore.amount - amount
    || destinationAfter.amount !== destinationBefore + amount
    || BigInt(protocolAfter.psmIdleUsdc.toString()) !== idleBefore - amount
  ) throw new Error(`PSM redemption verification failed for ${signature}`);

  return { signature, burnedNusd6: amount, receivedUsdc6: amount };
}

Outflow breaker preview

The active rolling-window limit is the smaller nonzero value of:

  • psmOutflowLimitUsdc; and
  • psmOutflowWindowBasisUsdc × psmOutflowLimitBps / 10,000.

Subtract psmOutflowWindowUsdc when the current window has not expired. This is only a preview: the program starts a new window using then-current idle liquidity when necessary, and concurrent redemptions can consume capacity. Always simulate.

On this page