Health, Max Amounts & E-mode
Read account health from the simulated health cache, size actions with computeMax*ForBank, and inspect e-mode state.
Every borrow and withdraw is gated by the on-chain risk engine, so before building one you usually want three numbers: how healthy the account is, how much it can move for a given bank, and whether e-mode changes the picture. All three are one call away on MarginfiAccountWrapper.
This page assumes you have a client and wrappedAccount from Getting Started. USD values are BigNumbers; amounts are BigNumbers in token UI units. For the protocol rules behind the numbers see Risk Engine.
The health cache
The program keeps a healthCache on every account: weighted asset and liability values at the three margin requirement levels, a timestamp, status flags and the prices it used. It is only refreshed when an instruction runs, so the stored copy goes stale between your actions.
The SDK therefore simulates the cache for you: client.fetchAccount(address) runs a pulse_health simulation against current oracle prices and returns a wrapper whose healthCache reflects right now. Pass skipHealthCache = true to get the raw on-chain values instead.
To refresh later (after prices moved, or after one of your transactions landed), re-simulate and rebuild the wrapper — accounts are immutable, simulateHealthCache() returns a new one:
import { MarginfiAccountWrapper } from "@0dotxyz/p0-ts-sdk";
const { account, error } = await wrappedAccount.simulateHealthCache();
if (error) {
// HealthCacheSimulationError: the on-chain simulation failed and the SDK fell back
// to a local computation. Values are still usable, just not program-verified.
console.warn("Health simulation fell back:", error.message, error.mrgnErr, error.internalErr);
}
wrappedAccount = new MarginfiAccountWrapper(account, client);wrappedAccount.account.healthCache.simulationStatus tells you where the numbers came from: HealthCacheStatus.ONCHAIN (simulation succeeded), COMPUTED (local fallback) or UNSET (nothing simulated yet — the cache-based getters below log a warning).
If you work with a bare MarginfiAccount instead of the wrapper, the same logic is exported as simulateAccountHealthCacheWithFallback({ program, banksMap, oraclePricesByBank, marginfiAccount, bankIntegrationMap, assetShareValueMultiplierByBank?, activeEmodeWeightsByBank? }) which returns { marginfiAccount, error? }. MarginfiAccount.simulateHealthCache(params) is the same call minus marginfiAccount.
Reading health
Once the cache is simulated, these getters are synchronous and cheap:
import { MarginRequirementType } from "@0dotxyz/p0-ts-sdk";
// Weighted USD values at each requirement level
const init = wrappedAccount.computeHealthComponentsFromCache(MarginRequirementType.Initial);
const maint = wrappedAccount.computeHealthComponentsFromCache(MarginRequirementType.Maintenance);
const equity = wrappedAccount.computeHealthComponentsFromCache(MarginRequirementType.Equity);
// Health factor: assets / liabilities (> 1 is healthy; < 1 at Maintenance means liquidatable)
const maintHealthFactor = maint.liabilities.gt(0)
? maint.assets.div(maint.liabilities)
: null;
// Free collateral (Initial assets - Initial liabilities), clamped at 0 by default
const freeCollateral = wrappedAccount.computeFreeCollateralFromCache();
const signedFreeCollateral = wrappedAccount.computeFreeCollateralFromCache({ clamped: false });
// Equity (unweighted assets - liabilities) and blended APY across positions
const accountValue = wrappedAccount.computeAccountValue();
const netApy = wrappedAccount.computeNetApy(); // decimal, e.g. 0.0421 = 4.21%
console.log(`Init ${init.assets.toFixed(2)} / ${init.liabilities.toFixed(2)}`);
console.log(`Maint ${maint.assets.toFixed(2)} / ${maint.liabilities.toFixed(2)} (HF ${maintHealthFactor?.toFixed(3) ?? "n/a"})`);
console.log(`Equity ${accountValue.toFixed(2)} USD, free collateral ${freeCollateral.toFixed(2)} USD, net APY ${(netApy * 100).toFixed(2)}%`);| Requirement | Used for | Cache fields |
|---|---|---|
Initial | Opening or growing positions (borrow, withdraw) | assetValue, liabilityValue |
Maintenance | Liquidation threshold | assetValueMaint, liabilityValueMaint |
Equity | Actual value, no risk weights | assetValueEquity, liabilityValueEquity |
A borrow or withdraw is accepted when Initial assets stay at or above Initial liabilities afterwards; an account becomes liquidatable when Maintenance assets drop below Maintenance liabilities. computeNetApy weights each position's supply/borrow APY by its USD value and includes any active e-mode weights.
computeHealthComponentsFromCache reads the cache exactly as the program would,
including the program's conservative price bias. For a purely local calculation
from balances and prices (no RPC), use computeHealthComponentsFromBalances
and computeFreeCollateralFromBalances on the underlying MarginfiAccount.
Max amounts
Three helpers return the largest amount the account can move for a bank right now, as a BigNumber in token UI units (not USD). Since SDK 2.7.1 they are bank-aware: they combine the health-based limit with the bank's caps, available liquidity and the bank/group rate limiters, mirroring the checks the program runs.
computeMaxBorrowForBank(bankAddress: PublicKey, opts?: { volatilityFactor?: number; ignoreBankLimits?: boolean }): BigNumber
computeMaxWithdrawForBank(bankAddress: PublicKey, opts?: { volatilityFactor?: number; ignoreBankLimits?: boolean }): BigNumber
computeMaxDepositForBank(bankAddress: PublicKey, opts?: { walletBalance?: BigNumber | number }): BigNumber// Largest borrow the account can take from this bank right now
const maxBorrow = wrappedAccount.computeMaxBorrowForBank(usdcBank.address, {
volatilityFactor: 0.95, // optional haircut on free collateral to leave a buffer
});
// Largest withdrawal that keeps the account healthy
const maxWithdraw = wrappedAccount.computeMaxWithdrawForBank(solBank.address);
// Remaining deposit cap, optionally min'd with the wallet balance
const maxDeposit = wrappedAccount.computeMaxDepositForBank(usdcBank.address, {
walletBalance: 1_000,
});What each one computes:
maxBorrow = max(0, min(healthBased, remainingBorrowCap, projectedAvailableLiquidity,
bankNetOutflowRateLimit, groupUsdRateLimit))
maxWithdraw = max(0, min(healthBased, projectedAvailableLiquidity,
bankNetOutflowRateLimit, groupUsdRateLimit))
maxDeposit = min(remainingDepositCap, walletBalance?)- Health-based borrow starts from free collateral (times
volatilityFactor, default 1) and converts it to tokens at the bank's liability weight and highest-bias price. Every liability-denominated bound is divided by(1 + protocolOriginationFee)because the program books the origination fee as extra liability. If the account already holds this asset, the collateral it frees up is added. Isolated-tier rules are applied first: borrowing an isolated asset while holding other debt, or a new asset while holding isolated debt, returns0. - Health-based withdraw is the amount whose removal keeps Initial health at or above zero, again with
volatilityFactorapplied to free collateral. - Caps are strict (
limit - total - 1) and buffered for interest; liquidity is projected through interest accrual; the rate limiters are the bank's net-outflow limiter (tokens) and the group's USD limiter converted at the unbiased realtime price. Deposits are not health-constrained, somaxDepositis just cap and wallet. - The wrapper passes
client.group.rateLimiterfor you and applies any active e-mode weights (see below). The standalone utils (computeMaxBorrowForBanketc. exported from the SDK) take those as explicit params.
Pass { ignoreBankLimits: true } to computeMaxBorrowForBank / computeMaxWithdrawForBank to get the purely health-based number. Use it for liquidation and flash-loan flows, which skip the rate limiters on-chain, or when you want to show "your collateral allows X, the pool currently allows Y" separately:
const healthOnly = wrappedAccount.computeMaxBorrowForBank(bankAddress, { ignoreBankLimits: true });
const poolLimited = wrappedAccount.computeMaxBorrowForBank(bankAddress);
if (poolLimited.lt(healthOnly)) {
console.log("Borrow is capped by bank limits, not by your collateral");
}The bank-side building blocks are exported if you want the individual terms: computeBankBorrowCapRemaining, computeBankDepositCapRemaining, computeBankProjectedAvailableLiquidity, computeBankRateLimitRemaining(bank, nowSeconds?) and computeGroupRateLimitRemainingUsd(client.group.rateLimiter, nowSeconds?) — see Banks & Prices and Caps & Rate Limits.
E-mode
E-mode raises the asset weights of a collateral bank when the account only borrows assets that are "paired" with it (for example SOL-family collateral against SOL debt). Pairs come from bank configuration; the wrapper derives the account's current state from client.emodePairs and its balances.
// Pairs currently active for this account (empty array = e-mode off)
const activePairs = wrappedAccount.getActiveEmodePairs();
// EmodePair { collateralBanks, collateralBankTag, liabilityBank, liabilityBankTag, assetWeightInit, assetWeightMaint }
// Effective (lowest) e-mode weights per collateral bank, keyed by bank address
const weights = wrappedAccount.getActiveEmodeWeightsByBank();
// Map<string, { assetWeightInit: BigNumber; assetWeightMaint: BigNumber }>These weights already flow into simulateHealthCache, computeNetApy and the max-amount helpers, so you rarely need them directly. What you do want before an action is to know whether it will change e-mode — that is what getEmodeImpacts() answers, for every bank the client knows:
getEmodeImpacts(): Record<string, ActionEmodeImpact>
interface ActionEmodeImpact {
borrowImpact?: EmodeImpact; // if you open a new borrow on this bank
supplyImpact?: EmodeImpact; // if you deposit into this (collateral-eligible) bank
repayAllImpact?: EmodeImpact; // if you fully repay this bank (only set for active liabilities)
withdrawAllImpact?: EmodeImpact; // if you fully withdraw this bank (only set for active assets)
}
interface EmodeImpact {
status: EmodeImpactStatus;
resultingPairs: EmodePair[]; // pairs that would be active after the action
activePair?: ActiveEmodePair; // merged view of those pairs
}
enum EmodeImpactStatus { ActivateEmode, ExtendEmode, IncreaseEmode, ReduceEmode, RemoveEmode, InactiveEmode }| Status | Meaning |
|---|---|
ActivateEmode | E-mode is off today and this action turns it on |
ExtendEmode | E-mode stays on with the same weights (same tag, more pairs) |
IncreaseEmode | E-mode stays on and the minimum initial weight goes up (better) |
ReduceEmode | E-mode stays on but the minimum initial weight goes down (worse) |
RemoveEmode | E-mode is on today and this action turns it off |
InactiveEmode | E-mode is off before and after |
A typical use is to list which borrows would activate or hurt e-mode before the user picks one:
import { EmodeImpactStatus, isStandardBorrowable } from "@0dotxyz/p0-ts-sdk";
const impacts = wrappedAccount.getEmodeImpacts();
for (const bank of client.banks.filter(isStandardBorrowable)) {
const impact = impacts[bank.address.toBase58()]?.borrowImpact;
if (!impact) continue; // already held as collateral, cannot borrow
switch (impact.status) {
case EmodeImpactStatus.ActivateEmode:
case EmodeImpactStatus.IncreaseEmode:
console.log(`${bank.tokenSymbol}: borrowing activates/improves e-mode (init weight ${impact.activePair?.assetWeightInit.toString()})`);
break;
case EmodeImpactStatus.ReduceEmode:
case EmodeImpactStatus.RemoveEmode:
console.log(`${bank.tokenSymbol}: borrowing reduces/removes e-mode`);
break;
default:
break; // ExtendEmode / InactiveEmode: no change worth flagging
}
}computeMaxBorrowForBank already uses borrowImpact internally: when the borrow would activate or change e-mode it recomputes free collateral from balances with the resulting weights instead of reading the cache, so the returned maximum reflects the post-action weights.
For custom pair sets (for example a hypothetical configuration) the wrapper also exposes computeActiveEmodePairs(emodePairs) and computeEmodeImpacts(emodePairs, banks). Protocol background: E-mode and the E-mode guide.
Related
- Deposit, Withdraw, Borrow & Repay — use the max amounts when building actions
- Banks & Prices — caps, liquidity and rate-limiter helpers per bank
- Risk Engine and E-mode — the protocol rules
- Liquidating accounts — where
ignoreBankLimitsand Maintenance health matter - Examples:
06a-account-health-simulated.ts,06b-account-health-calculated.ts