TypeScript SDK

Banks & Prices

Find banks, understand asset tags, read bank metrics and rate limiters, and work with oracle prices.

A bank is a lending pool for one mint with its own interest curve, caps, weights and oracle. This page is the home for everything bank- and price-related in the SDK. It assumes you have a client from Getting Started.

Finding banks

Project0Client.initialize loads every bank in the group once. You then read them from memory:

import { PublicKey } from "@solana/web3.js";
import { AssetTag } from "@0dotxyz/p0-ts-sdk";

// All banks as an array, or as a Map keyed by base58 address
const all = client.banks;
const byAddress = client.bankMap; // Map<string, Bank>

// One bank by address (returns `Bank | undefined`)
const bank = client.getBank(new PublicKey("BANK_ADDRESS"));
if (!bank) throw new Error("Bank not found");

// All banks for a mint, optionally filtered by asset tag
const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
const usdcBanks = client.getBanksByMint(USDC_MINT); // may include Kamino/Drift/JupLend wrappers
const [usdcBank] = client.getBanksByMint(USDC_MINT, AssetTag.DEFAULT);

Useful fields on a Bank: address, mint, mintDecimals, tokenSymbol, config (asset tag, weights, caps, operationalState, interestRateConfig, oracleKeys), rateLimiter, lastUpdate, and the share-conversion helpers getAssetQuantity(shares) / getLiabilityQuantity(shares).

Asset tags and comingling rules

The SDK uses AssetTag to determine what kinds of assets can be held together in a single account. This prevents incompatible positions from being comingled:

TagValueMeaning
AssetTag.DEFAULT0Regular assets (USDC, USDT, …) that can be comingled with other DEFAULT assets or with SOL
AssetTag.SOL1Only the native SOL bank. Accounts with SOL can hold either DEFAULT assets or STAKED assets, not both
AssetTag.STAKED2Native-stake LSTs. Accounts with STAKED positions can only deposit other STAKED assets or SOL, and can only borrow SOL
AssetTag.KAMINO3Kamino integration banks
AssetTag.DRIFT4Drift integration banks
AssetTag.SOLEND5Solend integration banks (not yet active)
AssetTag.JUPLEND6Jupiter Lend integration banks

Comingling rules:

  • DEFAULT assets can mix with other DEFAULT assets and SOL
  • SOL can mix with DEFAULT assets or STAKED assets (but not both in the same account)
  • STAKED assets can only mix with other STAKED assets or SOL
  • You cannot hold both DEFAULT and STAKED assets in the same account (unless only SOL is present)
const usdcBank = client.getBanksByMint(USDC_MINT, AssetTag.DEFAULT)[0];
const solBank = client.getBanksByMint(SOL_MINT, AssetTag.SOL)[0];

const lstBanks = client.banks.filter((b) => b.config.assetTag === AssetTag.STAKED);
const kaminoBanks = client.banks.filter((b) => b.config.assetTag === AssetTag.KAMINO);
const driftBanks = client.banks.filter((b) => b.config.assetTag === AssetTag.DRIFT);
const juplendBanks = client.banks.filter((b) => b.config.assetTag === AssetTag.JUPLEND);

Integration banks (Kamino, Drift, Jupiter Lend) reuse the same mint as a standard bank but are deposit-only wrappers with their own instructions — see Integrations.

Depositable and borrowable predicates

Two helpers tell you whether a bank accepts the standard deposit/borrow instructions, which is what you want when choosing a bank for a loop, swap or bridge hop:

import { isStandardDepositable, isStandardBorrowable } from "@0dotxyz/p0-ts-sdk";

// DEFAULT/SOL tag and Operational state
isStandardDepositable(bank);

// DEFAULT/SOL tag, Operational state, and borrowLimit > 0
isStandardBorrowable(bank);

Integration wrappers set borrowLimit = 0 and reject a standard borrow with program error 6200 WrongAssetTagForStandardInstructions; ReduceOnly/Paused banks reject new deposits with 6017 BankReduceOnly.

Bank metrics

computeBankMetrics aggregates every number a bank UI usually shows. It is pure: pass whatever (bank, oraclePrice) snapshot you want reflected.

import { computeBankMetrics } from "@0dotxyz/p0-ts-sdk";

const key = usdcBank.address.toBase58();
const metrics = computeBankMetrics({
  bank: usdcBank,
  oraclePrice: client.oraclePriceByBank.get(key)!,
  assetShareValueMultiplier: client.assetShareValueMultiplierByBank.get(key), // Kamino/Drift/JupLend only
  symbol: usdcBank.tokenSymbol,
});

console.log(`${metrics.symbol}: supply ${(metrics.supplyApy * 100).toFixed(2)}% / borrow ${(metrics.borrowApy * 100).toFixed(2)}%`);
console.log(`utilization ${(metrics.utilizationRate * 100).toFixed(1)}%, TVL $${metrics.totalDepositsUsd.toFixed(0)}`);

All amounts are UI-scaled numbers; USD values use the realtime oracle price with no margin weight.

Prop

Type

When you only need one or two numbers, call the per-metric helpers directly (no oracle math is run for rate or capacity helpers):

HelperReturns
computeBankSupplyApy(bank) / computeBankBorrowApy(bank)APY as a number fraction
computeInterestRates(bank){ lendingRate, borrowingRate } APRs as BigNumber
computeUtilizationRate(bank)BigNumber in [0, 1]
computeRemainingCapacity(bank){ depositCapacity, borrowCapacity } in native units, interest-buffered
computeBankDepositCapRemaining(bank) / computeBankBorrowCapRemaining(bank)UI number, Infinity when the cap is inactive
computeBankAvailableLiquidity(bank, multiplier?)BigNumber UI units: max(0, deposits − borrows) — what the vault can pay out now
computeBankProjectedAvailableLiquidity(bank, multiplier?)Same, minus the liquidity that interest accrual will consume before your transaction lands
computeBankTotalDeposits(bank, multiplier?) / computeBankTotalBorrows(bank)UI number

computeBankProjectedAvailableLiquidity is the one the wrapper's max-withdraw/max-borrow helpers use, because the program accrues interest before its liquidity check. The interest curve and caps are described on Lending & Borrowing and Caps & Rate Limits.

Rate limiters

Banks and the group can carry net-outflow rate limiters (hourly and daily sliding windows) that cap withdrawals plus borrows. The SDK mirrors the on-chain window math read-only:

import {
  computeBankRateLimitRemaining,
  computeGroupRateLimitRemainingUsd,
} from "@0dotxyz/p0-ts-sdk";

// Remaining bank-level outflow in UI units of the bank's mint, or null if no limiter is enabled
const bankRemaining = computeBankRateLimitRemaining(usdcBank);

// Remaining group-level outflow in USD, or null if the group has no limiter
const groupRemainingUsd = computeGroupRateLimitRemainingUsd(client.group.rateLimiter);

console.log(bankRemaining?.toFixed(2) ?? "no bank limiter", groupRemainingUsd?.toFixed(2) ?? "no group limiter");

Both accept an optional nowSeconds second argument (defaults to Date.now() / 1000). computeMaxBorrowForBank and computeMaxWithdrawForBank already take these into account — see Health & Max Amounts. Which flows are exempt and how windows roll over is explained on Caps & Rate Limits.

Oracle prices

The client fetches a price for every bank at initialization and keeps it in client.oraclePriceByBank (keyed by base58 bank address):

const oraclePrice = client.oraclePriceByBank.get(usdcBank.address.toBase58());
if (!oraclePrice) throw new Error("No price for bank");

console.log(`Realtime: $${oraclePrice.priceRealtime.price.toFixed(4)} ±${oraclePrice.priceRealtime.confidence.toFixed(4)}`);
console.log(`Weighted (EMA): $${oraclePrice.priceWeighted.price.toFixed(4)}`);
console.log(`Updated: ${new Date(oraclePrice.timestamp.toNumber() * 1000).toISOString()}`);

An OraclePrice has two PriceWithConfidence legs plus a timestamp:

Prop

Type

PriceWithConfidence is { price, confidence, lowestPrice, highestPrice } (all BigNumber), where lowestPrice/highestPrice are price ∓ confidence after the SDK applies the same confidence-interval scaling and cap as the program. The program values collateral at the low bound and liabilities at the high bound; use getPrice to pick the same way:

import { getPrice, getPriceWithConfidence, PriceBias } from "@0dotxyz/p0-ts-sdk";

const collateralPrice = getPrice(oraclePrice, PriceBias.Lowest); // realtime, low bound
const debtPrice = getPrice(oraclePrice, PriceBias.Highest, true); // weighted, high bound
const mid = getPrice(oraclePrice); // PriceBias.None, realtime

const realtimeLeg = getPriceWithConfidence(oraclePrice, false); // === oraclePrice.priceRealtime

Refreshing prices

The cached prices go stale in a long-running process. Refresh them with fetchOracleData and use the returned maps (the client's map is read-only, so either rebuild the client or keep your own map for computations):

import { fetchOracleData } from "@0dotxyz/p0-ts-sdk";

const { bankOraclePriceMap, mintOraclePriceMap } = await fetchOracleData(client.banks, {
  pythOpts: { mode: "on-chain", connection }, // or { mode: "api", pythOnchainData: {...}, stakedCollatData: {...} }
  swbOpts: { mode: "on-chain", connection },
  isolatedBanksOpts: { fetchPrices: true },
});

const freshUsdc = bankOraclePriceMap.get(usdcBank.address.toBase58());

fetchOracleData routes each bank by its oracle setup: zero-oracle banks get a zero price, fixed-price banks their constant, and Pyth/Switchboard banks are fetched from chain (mode: "on-chain") or from your own price API (mode: "api"). Oracle cranking — writing fresh Switchboard/Pyth updates on-chain before a borrow or withdraw — is handled by the action builders and documented on Transactions. How the protocol picks and validates oracles is on Oracles.

Bank state

bank.config.operationalState is an OperationalState enum value:

StateEffect
OperationalNormal operation
ReduceOnlyNo new deposits or borrows; withdraws and repays allowed
ReduceOnlyWithBorrowingPowerReduce-only, but existing deposits still count as collateral
PausedAll operations blocked
UninitializedAwaiting a Jupiter Lend seed deposit; all operations blocked
KilledByBankruptcyBank was wound down after a bankruptcy
CircuitBrokenHalted by the per-bank oracle circuit breaker (0.1.10+); risk-increasing operations are rejected until the price settles

CircuitBroken only decodes with SDK >= 2.7.0; see Program Upgrade 0.1.10. The protocol-level description of each state is on Bank States.

import { OperationalState } from "@0dotxyz/p0-ts-sdk";

const active = client.banks.filter((b) => b.config.operationalState === OperationalState.Operational);

On this page