NestDevelopers
Borrow nUSD

Repay debt

Repay accrued fees and principal partially or in full.

repay_nusd(amount) first checkpoints the current stability fee. Payment is then applied to accrued fees before principal. The fee portion is routed to protocol revenue destinations and the principal portion is burned.

Repayment does not require an oracle update. The position owner must sign and pay from that owner's nUSD token account.

TypeScript example

repay-debt.ts
import { BN } from "@coral-xyz/anchor";
import { PublicKey, Transaction } from "@solana/web3.js";
import { getAccount } from "@solana/spl-token";
import {
  ata,
  borrowVaultPda,
  marketBySymbol,
  simulateSignSend,
  toBaseUnits,
  type NestClient,
} from "../nest-client";

export async function repayDebt(
  client: NestClient,
  symbol: string,
  amount: string | "max",
) {
  const owner = client.wallet.publicKey;
  const market = marketBySymbol(client, symbol);
  const coreProgramId = new PublicKey(client.deployment.programs.nestCore);
  const protocol = new PublicKey(client.deployment.accounts.protocol);
  const collateralConfig = new PublicKey(market.collateralConfig);
  const vault = borrowVaultPda(coreProgramId, owner, collateralConfig);
  const nusdMint = new PublicKey(client.deployment.mints.nusd);
  const nusdTokenProgram = new PublicKey(client.deployment.tokenPrograms.nusd);
  const ownerNusdAccount = ata(nusdMint, owner, nusdTokenProgram);

  const [vaultBefore, walletNusd] = await Promise.all([
    client.core.account.vault.fetch(vault),
    getAccount(client.connection, ownerNusdAccount, "confirmed", nusdTokenProgram),
  ]);
  const debtBefore = BigInt(vaultBefore.principalDebt.toString())
    + BigInt(vaultBefore.accruedFee.toString());
  if (debtBefore === 0n) throw new Error(`${symbol} position has no debt`);

  // For max repayment, passing the wallet balance is safe: Nest only transfers
  // the final amount owed after accruing to the transaction timestamp.
  const requestedAmount = amount === "max"
    ? walletNusd.amount
    : toBaseUnits(amount, 6);
  if (requestedAmount <= 0n) throw new Error("Repayment must be positive");
  if (requestedAmount > walletNusd.amount) throw new Error("Insufficient nUSD balance");

  const transaction = new Transaction().add(await client.core.methods
    .repayNusd(new BN(requestedAmount.toString()))
    .accountsStrict({
      protocol,
      collateralConfig,
      vault,
      nusdMint,
      ownerNusdAccount,
      insuranceNusdVault: new PublicKey(client.deployment.vaults.insuranceNusdVault),
      stakerRevenueNusdVault: new PublicKey(client.deployment.vaults.stakerRevenueNusdVault),
      protocolRevenueNusdVault: new PublicKey(client.deployment.vaults.protocolRevenueNusdVault),
      stakingState: new PublicKey(client.deployment.accounts.stakingState),
      owner,
      nusdTokenProgram,
    })
    .instruction());

  const signature = await simulateSignSend(client, transaction);
  const [vaultAfter, walletAfter] = await Promise.all([
    client.core.account.vault.fetch(vault),
    getAccount(client.connection, ownerNusdAccount, "confirmed", nusdTokenProgram),
  ]);
  const principalAfter = BigInt(vaultAfter.principalDebt.toString());
  const feeAfter = BigInt(vaultAfter.accruedFee.toString());
  const debtAfter = principalAfter + feeAfter;
  const paidNusd6 = walletNusd.amount - walletAfter.amount;
  if (paidNusd6 <= 0n || paidNusd6 > requestedAmount) {
    throw new Error(`Repayment verification failed for ${signature}`);
  }

  return {
    signature,
    principalDebtNusd6: principalAfter,
    accruedFeeNusd6: feeAfter,
    paidNusd6,
    fullyRepaid: debtAfter === 0n,
  };
}

Partial repayment

If the amount is smaller than the current accrued fee, principal does not change. Always display principal and fee separately after confirmation so the user can see how the payment was allocated.

Full repayment

  1. Use Calculate accrued stability fees to estimate the final debt.
  2. Confirm the wallet holds more nUSD than that estimate.
  3. Call the example with amount: "max".
  4. Refetch the vault and require both principalDebt and accruedFee to be zero.

The excess wallet balance is not transferred. After the debt is zero, all collateral can be withdrawn without consuming an oracle price.

On this page