Documentation Index
Fetch the complete documentation index at: https://kamino.com/docs/llms.txt
Use this file to discover all available pages before exploring further.
Deleveraging a multiply position reduces your leverage by selling collateral to repay debt in a single atomic transaction. This operation uses flash loans and KSwap to convert TSLAx collateral into USDC to repay debt without requiring external funds.
Multiply positions use MultiplyObligation which derives a unique PDA (Program Derived Address) based on your wallet, market, collateral token (TSLAx), and debt token (USDC). Each collateral/debt pair creates a separate obligation address, allowing you to have multiple multiply positions simultaneously.
Deleverage Multiply Position with xStocks
Reduce leverage on a TSLAx multiply position by selling collateral to repay USDC debt.
Import Dependencies
Import the required packages for Solana RPC communication, Kamino SDK operations, KSwap routing, Scope oracle, and transaction building.import {
createSolanaRpc,
createSolanaRpcSubscriptions,
address,
pipe,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
signTransactionMessageWithSigners,
sendAndConfirmTransactionFactory,
getSignatureFromTransaction,
none,
compressTransactionMessageUsingAddressLookupTables,
} from '@solana/kit';
import type { Address } from '@solana/kit';
import {
KaminoMarket,
MultiplyObligation,
PROGRAM_ID,
parseKeypairFile,
getWithdrawWithLeverageIxs,
getUserLutAddressAndSetupIxs,
getScopeRefreshIxForObligationAndReserves,
getComputeBudgetAndPriorityFeeIxs,
lamportsToNumberDecimal,
simulateTx,
DEFAULT_RECENT_SLOT_DURATION_MS,
} from '@kamino-finance/klend-sdk';
import { KswapSdk } from '@kamino-finance/kswap-sdk';
import { Scope } from '@kamino-finance/scope-sdk';
import Decimal from 'decimal.js';
import { getKswapQuoter, getKswapSwapper } from './kswap_utils.js';
import { fetchAllAddressLookupTable } from '@solana-program/address-lookup-table';
Load Configuration and Initialize SDKs
Load the keypair and initialize RPC connections, market, Scope oracle, and KSwap SDK.const KEYPAIR_FILE = '/path/to/your/keypair.json';
const CDN_ENDPOINT = 'https://cdn.kamino.finance';
const signer = await parseKeypairFile(KEYPAIR_FILE);
const rpc = createSolanaRpc('https://api.mainnet-beta.solana.com');
const rpcSubscriptions = createSolanaRpcSubscriptions('wss://api.mainnet-beta.solana.com');
const marketPubkey = address('5wJeMrUYECGq41fxRESKALVcHnNX26TAWy4W98yULsua'); // xStocks Market
const debtTokenMint = address('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'); // USDC
const XSTOCKS_MARKET_LUT = address('8ofreL6hKfEet1DnhHVGvCTnSdz4pg85PpbuCUHnEcKm'); // xStocks Market LUT
const market = await KaminoMarket.load(rpc, marketPubkey, DEFAULT_RECENT_SLOT_DURATION_MS);
const scope = new Scope('mainnet-beta', rpc);
const kswapSdk = new KswapSdk('https://api.kamino.finance/kswap', rpc, rpcSubscriptions);
Find TSLAx Reserve and Fetch Multiply LUTs
Dynamically discover the TSLAx reserve and fetch multiply-specific lookup tables from Kamino’s CDN.const tslaReserve = Array.from(market!.reserves.values()).find((reserve) => reserve.symbol === 'TSLAx');
if (!tslaReserve) {
console.log('TSLAx reserve not found in xStocks market');
}
const collTokenMint = tslaReserve.getLiquidityMint();
const kaminoResourcesResponse = await fetch(`${CDN_ENDPOINT}/resources.json`);
const kaminoResourcesData = await kaminoResourcesResponse.json();
const kaminoResources = kaminoResourcesData['mainnet-beta'];
const multiplyColPairs = kaminoResources.multiplyLUTsPairs[collTokenMint] || {};
const multiplyLut = multiplyColPairs[debtTokenMint] || [];
const multiplyLutKeys = multiplyLut.map((lut: string) => address(lut));
Set the withdrawal amount and slippage tolerance.const withdrawAmount = new Decimal(3); // $3 worth to deleverage
const slippageBps = 100; // 1% slippage (xStocks may need higher slippage)
Some assets may require higher slippage than others.
Get User Lookup Table
Retrieve the user-specific lookup table created during the first multiply deposit.const multiplyMints: { coll: Address; debt: Address }[] = [{ coll: collTokenMint, debt: debtTokenMint }];
const leverageMints: { coll: Address; debt: Address }[] = [];
const [userLookupTable] = await getUserLutAddressAndSetupIxs(
market!,
signer,
none(),
true,
multiplyMints,
leverageMints
);
Fetch Multiply Obligation and Position Data
Create the multiply obligation type, derive its PDA, and fetch current position data.const currentSlot = await rpc.getSlot().send();
const collTokenReserve = market!.getReserveByMint(collTokenMint)!;
const debtTokenReserve = market!.getReserveByMint(debtTokenMint)!;
const obligationType = new MultiplyObligation(collTokenMint, debtTokenMint, PROGRAM_ID);
const obligationAddress = await obligationType.toPda(market!.getAddress(), signer.address);
const obligation = await market!.getObligationByAddress(obligationAddress);
if (!obligation) {
console.log('No multiply obligation found. You must have an active multiply position to deleverage.');
}
const deposited = lamportsToNumberDecimal(
Array.from(obligation.deposits.values())[0]?.amount.toString() || '0',
collTokenReserve.state.liquidity.mintDecimals.toNumber()
);
const borrowed = lamportsToNumberDecimal(
Array.from(obligation.borrows.values())[0]?.amount.toString() || '0',
debtTokenReserve.state.liquidity.mintDecimals.toNumber()
);
console.log(`Current position: ${deposited.toString()} TSLAx deposited, ${borrowed.toString()} USDC borrowed`);
The obligation PDA is derived from the market address, wallet address, collateral mint (TSLAx), and debt mint (USDC). This means a TSLAx/USDC multiply position has a different address than a NVDAx/USDC multiply position for the same wallet.
Setup Scope Oracle and Price Data
Get Scope oracle refresh instructions and price ratios for the swap.const scopeConfiguration = { scope, scopeConfigurations: await scope.getAllConfigurations() };
const scopeRefreshIx = await getScopeRefreshIxForObligationAndReserves(
market!,
collTokenReserve,
debtTokenReserve,
obligation,
scopeConfiguration
);
const collPriceUsd = collTokenReserve.getOracleMarketPrice();
const debtPriceUsd = debtTokenReserve.getOracleMarketPrice();
const priceCollToDebt = collPriceUsd.div(debtPriceUsd);
Build Deleverage Instructions with Multiple Routes
Set compute budget and build withdrawal with leverage instructions using KSwap for multi-route optimization.const computeIxs = getComputeBudgetAndPriorityFeeIxs(1_400_000, new Decimal(500000));
const userSolBalanceLamports = Number.parseInt(
(await rpc.getBalance(signer.address).send()).value.toString()
);
const withdrawWithLeverageRoutes = await getWithdrawWithLeverageIxs({
owner: signer,
kaminoMarket: market!,
debtTokenMint: debtTokenMint,
collTokenMint: collTokenMint,
obligation: obligation,
deposited: deposited,
borrowed: borrowed,
referrer: none(),
currentSlot,
withdrawAmount,
priceCollToDebt,
slippagePct: new Decimal(slippageBps / 100),
isClosingPosition: false, // Set to true to close the entire position
selectedTokenMint: debtTokenMint, // Withdraw into USDC
budgetAndPriorityFeeIxs: computeIxs,
scopeRefreshIx,
quoteBufferBps: new Decimal(1000),
quoter: getKswapQuoter(kswapSdk, signer.address, slippageBps, collTokenReserve, debtTokenReserve),
swapper: getKswapSwapper(kswapSdk, signer.address, slippageBps),
useV2Ixs: true,
userSolBalanceLamports,
});
getWithdrawWithLeverageIxs handles the complex flow: flash borrow debt token → repay debt → withdraw freed collateral → swap collateral to debt token → repay flash loan. All in one atomic transaction.
Simulate Routes and Select Best
Prepare lookup tables, simulate all routes, and select the best performing route based on price.const klendLookupTableKeys: Address[] = [];
klendLookupTableKeys.push(userLookupTable);
klendLookupTableKeys.push(...multiplyLutKeys);
klendLookupTableKeys.push(XSTOCKS_MARKET_LUT);
const klendLutAccounts = await fetchAllAddressLookupTable(rpc, klendLookupTableKeys);
const simulationResults = await Promise.all(
withdrawWithLeverageRoutes.map(async (route) => {
const lookupTables = route.lookupTables;
lookupTables.push(...klendLutAccounts);
const simulation = await simulateTx(rpc, signer.address, route.ixs, lookupTables).catch(() => undefined);
if (!simulation || simulation.value.err) {
return undefined;
}
return {
ixs: route.ixs,
luts: lookupTables.map((l) => l.address),
routeOutput: route.quote!,
swapInputs: route.swapInputs,
};
})
);
const passingSimulations = simulationResults.filter((tx) => tx !== undefined);
const bestRoute =
passingSimulations.length > 0
? passingSimulations.reduce((best, current) => {
const inputMintReserve = market!.getReserveByMint(best.swapInputs.inputMint)!;
const outputMintReserve = market!.getReserveByMint(best.swapInputs.outputMint)!;
const bestPrice = new Decimal(best.routeOutput.amountsExactIn.amountOutGuaranteed.toString())
.div(outputMintReserve.getMintFactor())
.div(new Decimal(best.routeOutput.amountsExactIn.amountIn.toString()).div(inputMintReserve.getMintFactor()));
const currentPrice = new Decimal(current.routeOutput.amountsExactIn.amountOutGuaranteed.toString())
.div(outputMintReserve.getMintFactor())
.div(
new Decimal(current.routeOutput.amountsExactIn.amountIn.toString()).div(inputMintReserve.getMintFactor())
);
return bestPrice.greaterThan(currentPrice) ? best : current;
})
: (() => {
const lookupTables = withdrawWithLeverageRoutes[0].lookupTables;
lookupTables.push(...klendLutAccounts);
return { ixs: withdrawWithLeverageRoutes[0].ixs, luts: lookupTables.map((l) => l.address) };
})();
if (!bestRoute) {
console.log('No route found');
}
Simulation-based route selection ensures the transaction will succeed before sending it. Routes that fail simulation are filtered out, and the best price among passing routes is selected.
Build, Sign, and Send Transaction
Wait briefly to avoid rate limiting, get a fresh blockhash, compress the transaction with LUTs, and send it.await new Promise((resolve) => setTimeout(resolve, 2000));
const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'finalized' }).send();
const lutsByAddress: Record<Address, Address[]> = {};
const bestRouteLutAccounts = await fetchAllAddressLookupTable(rpc, bestRoute.luts || []);
for (const acc of bestRouteLutAccounts) {
lutsByAddress[acc.address] = acc.data.addresses;
}
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions(bestRoute.ixs, tx),
(tx) => setTransactionMessageFeePayerSigner(signer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => compressTransactionMessageUsingAddressLookupTables(tx, lutsByAddress)
);
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
const signature = getSignatureFromTransaction(signedTransaction);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction, {
commitment: 'processed',
preflightCommitment: 'processed',
skipPreflight: true,
});
console.log(`Multiply deleverage successful! Sold ~$${withdrawAmount.toString()} of collateral to repay debt.`);
console.log(`Transaction signature: ${signature}`);
The deleverage transaction is complete. Your leverage is reduced, debt is partially repaid, and remaining value is available as USDC or collateral based on the selectedTokenMint parameter.
Borrow and Multiply SDK Methods
| Method | Operation | When to Use |
|---|
getDepositWithLeverageIxs | Open position or add collateral | Creating or adding to a leveraged position |
buildRepayTxns | Repay with wallet funds | Reducing debt using USDC, SOL, etc. from wallet |
buildWithdrawTxns | Withdraw collateral | Withdrawing collateral after debt is repaid |
getWithdrawWithLeverageIxs | Deleverage and withdraw | Sell collateral to repay debt and receive remaining value in wallet |
getRepayWithCollIxs | Repay debt with collateral | Sell collateral to repay debt without withdrawing (value stays in position) |
Multiply operations use MultiplyObligation. Vanilla operations use VanillaObligation. The obligation type determines which PDA is derived for your position.