DecimalString contract and the naming of these helpers. Every formula takes and gives DecimalString values, throws PerpsError with ValidationError on a malformed or zero-divisor input, and has a safe* pair (for example safeCalculateRoe) that logs a warning and gives undefined in place of the throw. getGasRecommendation, the signing helpers, and the storage adapters perform network, crypto, or storage I/O.
Position Calculations
calculateSize
Calculate position size from margin, leverage, and price.
Returns:
DecimalString, divided to 40 decimal places.
calculateNotionalValue
Notional value in USD:|size| * price.
calculateUnrealizedPnl
(currentPrice - entryPrice) * size. size is signed: positive for long, negative for short.
calculateRoe
Return on equity as a percentage ('10' means 10%): pnl / margin * 100. Throws when margin is zero.
calculateRequiredMargin
notionalValue / leverage. Throws when leverage is zero.
calculateRealizedPnlPercent
Realised PnL as a percentage of the position value at close:realizedPnl / (|size| * price) * 100. Throws when size * price is zero.
calculateEffectiveLeverage
Effective leverage of an open position (positionValueUsd / marginUsd). Throws when marginUsd is zero. Takes a single params object.
calculateLiquidationDistance
Absolute distance from the current price to the liquidation price, as a percentage of the current price. Throws when the current price is zero.Margin adjustment capability
isPositionMarginAdjustable(position) is true when the position is isolated and its market’s positionMarginAdjustment is not NONE. isPositionMarginRemovable(position) is true when that field is ADD_AND_REMOVE; an ADD_ONLY market takes adds but no withdrawal.
To read how much margin can be withdrawn, call perps.getPositionRemovableMargin(position). It delegates to the position’s provider.
DecimalString | undefined. undefined for cross positions and markets without individual margin adjustment. '0' when the market is add-only or nothing is removable. Otherwise a decimal string rounded down to 6 decimal places. Throws PerpsError with ValidationError when a Position decimal the venue formula reads is malformed.
Each provider package also exports the same calculation as positionRemovableMargin(position):
Position Math (Estimates)
Pure-function helpers for estimating how a perp position changes after a fill. Add-to-position and partial-close preview blocks use them. These take SDK shapes as they are: passPosition.size, entryPrice, and the other decimal strings without conversion. Sizes passed in are non-negative magnitudes; direction is carried by isLong. Results are for display. Do not send them to a venue.
estimateLiquidationPrice
Estimated liquidation price for a new isolated-margin position, parameterised by the venue’s maintenance margin rate. ThrowsPerpsError with ValidationError when the inputs cannot produce one (zero leverage, zero denominator). safeEstimateLiquidationPrice gives undefined instead. For existing positions, prefer Position.liquidationPrice from the venue.
Returns:
DecimalString
estimateLiquidationPriceAtMarketRate(market, params) reads the rate from market.maintenanceMarginRate and gives undefined when the market has none.
wouldImmediatelyLiquidate({ liquidationPrice, currentPrice, isLong }) is true when a long’s liquidation price is at or above the current price, or a short’s is at or below it. It gives false when either price is not positive.
estimateAverageEntryPrice
Estimated average entry price after adding to an existing position. Weighted average of current entry and the new fill price, weighted by leg size in asset units. Both legs must be in the same direction (this helper is for adding to, not flipping, a position).
Returns:
DecimalString — the new weighted-average entry price. Throws PerpsError with ValidationError when the combined size is not positive or a value is malformed.
estimateNewLeverage
Estimated effective leverage (totalNotional / totalMargin) after adding margin and notional. The caller computes notional from size and price (e.g. with calculateNotionalValue) and supplies the additional margin the user is about to put up.
Returns:
DecimalString — the new effective leverage. Throws PerpsError with ValidationError when the total margin is not positive.
estimateUnrealizedPnl
Estimated unrealised PnL at the current mark price:(markPrice - entryPrice) * size * (isLong ? 1 : -1).
Returns:
DecimalString
calculateRealizedPnl
Realised PnL on the portion of a position being closed:(closePrice - entryPrice) * closeSize * (isLong ? 1 : -1).
Returns:
DecimalString
Order Math (Expected rPnL Previews)
Helpers for previewing the realised PnL the user would lock in if an active order filled against a matching position.estimateRealizedPnl composes calculateRealizedPnl from Position Math, so the two stay in lockstep.
Orders that open or add to a position have no defined rPnL and return undefined.
resolveMatchingPosition
Pick the matching open position for an order’s asset, if any.
Returns:
Position | undefined
resolveCloseSize
Resolve the close-size against a position, applying the spec’s cap rules:orderSize === 0is the Hyperliquid convention for “close entire position” used by trigger orders → close the full position size.- Otherwise cap at the absolute position size; an order larger than the position can only close what’s open.
Returns:
DecimalString
estimateRealizedPnl
Expected rPnL for an unfilled order against a matching position.estimateRealizedPnl(order, position) takes any Order. A resting limit order prices off its limit price and an armed trigger order prices off its trigger price. A TWAP parent carries no single execution price, so the result is undefined. A malformed decimal on the order or the position throws PerpsError with ValidationError; safeEstimateRealizedPnl gives undefined instead.
Returns:
DecimalString | undefined
For a market or limit order (RegularOrder), reducing requires opposite sides (long position + SELL, short position + BUY). The projected quantity is order.remainingSize, capped at the position size, at order.price. The result is undefined when:
- there is no matching position;
order.statusis not inACTIVE_ORDER_STATUSES;- the order is same-side (it adds to the position);
order.priceisnull, or the position size is not positive;remainingSizeis0.
TriggerOrder), the order is a closing leg, so its side must be opposite to the position’s. The trigger price is the rPnL price. TriggerOrder.triggerPrice is optional, and the result is undefined when it is absent. The optional limitPrice for STOP_LIMIT / TAKE_PROFIT_LIMIT is the post-trigger limit, not the rPnL price. The projected quantity is remainingSize, capped at the position size. The result is undefined when:
- there is no matching position;
order.statusis not inACTIVE_ORDER_STATUSES, or isPENDING;- the order side is not opposite to the position;
- the position size is not positive;
remainingSizeis0, unlessoriginalSizeis0and the order isreduceOnly(the close-entire-position convention, which projects the full position size).
TP/SL Calculations
calculateExpectedPnl
Calculate expected PnL if a trigger price is hit.
Returns:
ExpectedPnl — { amount: DecimalString; percent: DecimalString } (signed; positive = profit). Throws PerpsError with ValidationError when triggerPrice, entryPrice, or margin is zero.
calculateTriggerPrice
Convert a target PnL percentage (leveraged ROE, so50 is +50% on margin) to a trigger price: entryPrice ± entryPrice * percent / 100 / leverage, plus for a long and minus for a short. Throws when entryPrice or leverage is zero.
calculateTriggerPercent
Convert a trigger price to a leveraged PnL percentage:(price - entryPrice) / entryPrice * leverage * 100, sign-flipped for a short. Throws when entryPrice or leverage is zero.
Decimal Arithmetic
Exact arithmetic onDecimalString values. Each helper throws PerpsError with ValidationError for a malformed input, and has a safe* pair.
Order Entry Amounts
calculateOrderAmounts({ sdk, market, held, amount, leverage, price }) derives the three order-entry amounts from the one the user typed. held is 'margin', 'size', or 'notional', and amount is that figure as a DecimalString. leverage is a number. It returns { margin, size, notional } as DecimalString values.
- Only
sizesnaps, onto the market’s lot grid through the market’s own provider. An order sends size alone. notionalis the snappedsize * price.marginis the heldamountunchanged whenheldis'margin'. Otherwise it isnotional / leverageand is for display only.
PerpsError with ValidationError when amount or price is not a positive decimal, leverage is not a finite number above zero, or the snapped size is zero. It throws SDKError when market.providerId names no registered provider. safeCalculateOrderAmounts gives undefined instead.
Balance Helpers
calculateTransferable(venueFigure, units)clamps the venue’s free figure to the range from'0'tounits. It is the rule behindBalance.transferable.calculateWithdrawMax(row)gives the largestamountaWithdrawableBalancerow can fund. It isavailable, oravailable - withdrawalFeefloored at'0'whenisFeeDeductedisfalse.calculateRefuelAmount({ recommendedAmount, recommendedUsd, nativeBalance, priceUsd, decimals })gives the source-token amount that buys the native gas the wallet lacks. It addsREFUEL_FEE_MARGIN_PERCENT(20) percent, rounds up todecimalsplaces, and givesundefinedwhen the wallet already holds the recommendation.recommendedAmountandnativeBalanceare integer base-unit strings.
Fee & Slippage
estimateFees
sizeUsd * feeRate. Both are DecimalString values. feeRate is a decimal fraction ('0.0005' is 0.05%).
calculateSlippagePrice
Adjust a price by a slippage tolerance.calculateSlippagePrice(price, slippagePercent, isBuy) takes DecimalString values. slippagePercent is a percentage ('0.5' is 0.5%; '1' is 1%). A buy multiplies the price by 1 + p / 100; a sell divides it by 1 + p / 100. The result is not rounded to a tick grid: pass it to snapOrderPrice. It throws when slippagePercent is -100 or less.
Quote Building
Pure helpers behindgetQuote / subscribeQuote — walk a book to a VWAP fill and assemble a Quote. Most consumers call the client methods; reach for these to compute a quote from a book snapshot you already hold.
calculateOrderbookWalk
Walk one side of an orderbook to fillsizeUsd notional, accumulating base size and notional level-by-level to derive the VWAP fill. Levels are consumed in array order — pass asks for a buy and bids for a sell, each ordered best-price-first. Throws PerpsError (ValidationError) on a malformed level.
Returns:
BookWalk — { baseSize, filledNotional, vwap, insufficientLiquidity }. baseSize, filledNotional, and vwap are DecimalString values (vwap is '0' for an empty book). insufficientLiquidity is a boolean and is true when the book cannot absorb the full notional (the walk returns the best obtainable fill).
buildQuote
Assemble aQuote from a resolved market, its live MarketContext, and its orderbook snapshot: walks the relevant side for the VWAP fill, derives price impact in basis points versus mark, and applies the base taker fee on the filled notional. Throws PerpsError (ValidationError) on a malformed book level.
Quote.
Account Summary
Derive portfolio metrics with thePerpsClient.getAccountSummary() method:
Returns:
AccountSummary — { portfolioValue, availableMargin, marginUsed, unrealizedPnl }, all string fields. Each provider computes the roll-up from its own venue figures — see getAccountSummary for the source of each field.
Activity Pagination
paginateActivity merges a provider’s freshly-fetched activity rows with the replayed overflow tail from the previous page, applies the request’s type and time filters, sorts newest-first, and slices to the page limit. It is exported for provider authors implementing a custom getActivity; most consumers call getActivity() directly and never call this helper.
Returns:
{ items: ActivityItem[]; pagination: Pagination }.
Order Classification
isTakeProfitOrder / isStopLossOrder / isTpSlOrder
Check whether an order is a TP, SL, or either. They accept anything with an ordertype.
isTriggerOrder / isRegularOrder / isTwapOrder
Type guards that narrow theOrder union. isTriggerOrder matches TriggerOrder (take-profit and stop types), isRegularOrder matches RegularOrder (MARKET and LIMIT), and isTwapOrder matches TwapOrder (TWAP).
triggerConditionFor
triggerConditionFor(type, side) returns the TriggerCondition a trigger order fires on. Take-profit on a sell and stop-loss on a buy return ABOVE; take-profit on a buy and stop-loss on a sell return BELOW.
classifyFillFromPosition
Classify a fill into the full Open/Close/Increase/Reduce/Switch taxonomy using the signed position held before the fill. This is the SDK classifier for a fill when the position before the fill is known.
Returns:
FillClassification. A string that does not match the decimal pattern gives FillClassification.BUY or FillClassification.SELL by side.
ACTIVE_ORDER_STATUSES / isActiveOrderStatus
ACTIVE_ORDER_STATUSES is the ReadonlySet<OrderStatus> of lifecycle statuses included in order reads by default: OPEN, PENDING, ACCEPTED, PARTIALLY_FILLED, TRIGGERED. An order in one of these is still live (not terminal): PENDING waits on a parent, and ACCEPTED is venue-accepted but not yet on the book. Any other status is terminal; evict it from a cached orders list on a WS update. isActiveOrderStatus(status) is the membership check.
orderSideLabel
orderSideLabel(side, market) returns the direction label of an order. A spot market gives 'Buy' or 'Sell'. A perp market gives 'Long' or 'Short'. The label comes only from side and market.categoryId. reduceOnly and isLiquidation do not change it.
Returns:
OrderSideLabel, the type 'Long' | 'Short' | 'Buy' | 'Sell'. Import the type from @lifi/perps-sdk, for example to select the color of a label.
SPOT_CATEGORY_ID / isSpotCategory
SPOT_CATEGORY_ID is the category id ('spot') of spot markets and spot balances on every provider. isSpotCategory(categoryId) is the equality check. Use it in place of a literal 'spot' comparison.
Parsing & Conversion
safe* pair (for example safeRoundDecimalString) that logs a warning and gives undefined in place of the throw.
decimalStringToNumber
Read aDecimalString as a number. Use it for chart values and other display input only, because a number loses digits. It throws PerpsError with ValidationError for a string that does not match the decimal pattern or a value too large for a number.
isDecimalString
Narrows anunknown value to DecimalString. See Numbers for the pattern.
unknownToDecimalString
unknownToDecimalString(value, field, tool) turns an unknown venue value into a DecimalString. It spells a finite number or an exponent string in plain notation. It throws PerpsError with SDKError and a message that names field and tool when the value is not a finite decimal.
roundDecimalString
Round aDecimalString to decimals places with an explicit DecimalRounding. 'truncate' moves toward zero, 'round' goes half away from zero, and 'up' goes away from zero. It strips trailing zeros. It throws PerpsError with ValidationError for a value that does not match the decimal pattern, or a decimals that is not a non-negative integer.
decimalStringToScaledInteger
Scale aDecimalString to an integer number in exact decimal arithmetic. Use it when a provider wire format requires an integer amount. decimalStringToScaledInteger(value, decimals, rounding) requires an explicit off-grid rounding (type DecimalRounding): 'truncate' moves toward zero for sizes and collateral, while 'round' snaps prices half away from zero.
value that does not match the decimal pattern, a negative or non-integer decimals, and a result beyond Number.MAX_SAFE_INTEGER throw PerpsError with ValidationError.
scaledIntegerToDecimalString
Convert a scaled integer amount (an integer string) to aDecimalString. It throws PerpsError with ValidationError when amount is not an integer string ('1.5', '1e3', '', 'abc') or decimals is not a non-negative integer.
numberToDecimalString
Spell anumber as a DecimalString in plain notation, never in exponent notation. Use it at a boundary where a venue or browser API gives a number. It throws PerpsError with ValidationError for NaN and infinite values.
Formatting
Display formatters for USD amounts, prices, and percentages. Each accepts aFormatInput (number, decimal string, or null/undefined) and an optional FormatOptions. Null, blank, and non-finite input renders the placeholder (default '—'). A string that does not match the decimal pattern is returned unchanged.
FormatOptions is accepted by every formatter; the defaults differ by formatter:
formatCompactUsd reads only decimals and placeholder; it ignores locale and rounding.
formatNumber
Currency-symbol-free core formatter: two decimals with locale digit grouping by default. The other formatters build on it.formatUsd
Unsigned USD, two decimals with digit grouping. Negatives place the- before the $.
formatSignedUsd
USD with an explicit sign, derived after rounding (so sub-cent magnitudes render$0.00).
formatSignedPercent
Percentage with an explicit sign and no grouping.formatPrice
Unsigned price with decimals auto-detected from magnitude (override withoptions.decimals): 2 decimals at >= 1, 4 at >= 0.1, 5 at >= 0.01, otherwise 6. Grouping applies once the absolute value reaches 1000.
formatCompactUsd
USD with aB/M/K suffix, using decimals (default 2) for every suffix. Values below 1000 render without a suffix. There is no promotion at a rounding boundary: formatCompactUsd(999_999) renders '$1000.00K'.
Explorer
explorerTxUrl / ExplorerChainId
Resolve a tx hash to a fully-qualified block-explorer URL for the settling chain.ExplorerChainId is the const map of supported settling chains and the union of its values: ETHEREUM 1 (etherscan.io), ARBITRUM_ONE 42161 (arbiscan.io), LIGHTER 304 (app.lighter.xyz), ROBINHOOD 4663 (robin.etherscan.io), HYPERLIQUID 999 (app.hyperliquid.xyz), AVALANCHE_C_CHAIN 43114 (snowtrace.io), and BNB_SMART_CHAIN 56 (bscscan.com). explorerTxUrl returns undefined for an empty hash (no on-chain tx to link).
Returns:
string | undefined. Use explorerTxUrlFromBase(baseUrl, txHash) for provider instances whose explorer is configured by URL rather than one of the known ExplorerChainId values; it returns undefined when either input is absent.
Deposit Assets and Gas
Deposit destinations are provider-owned and resolved throughPerpsClient.getDepositFlow(). The SDK exports canonical on-chain asset identities for consumers that render or execute those flows:
DeclaredDepositAsset with { chainId, address, decimals }; identity is the chain/address pair, not the display symbol. getGasRecommendation(client, { chainId }) calls LI.FI’s gas suggestion API for the native-gas leg of a venueDepositPipeline. It returns GasRecommendationResponse, including available: false when LI.FI cannot source gas for that chain. The call goes to LIFI_API_URL (https://li.quest/v1) — the general LI.FI API, not the perps backend apiUrl.
Setup and Market Selection
selectUserSetupActions(provider.setup)returns the steps a user sees: every choice step, and every non-choice step whosesignerisUSER. A non-choice step whosesignerisSDKis completed inline bycheckSetup()and should not be rendered as a user action. When an account is available, prefercheckSetup().checklist, which also carries each step’s satisfied state.isActiveMarket(market)is false only whenmarket.isDelisted === true.toPerpsMarketDisplay(market)projects a perpetual market to the identity/capability shape embedded in positions, includingisDelistedandpositionMarginAdjustment.toAssetDisplay(asset)projects anAssetto theAssetDisplayidentity and label shape.
Default Markets
DEFAULT_MARKET_ID maps a provider key to the venue Market.id to land on when no market is named: hyperliquid 'BTC', lighter '1', ondo 'BTC-USD.P'. getDefaultMarketId(provider) reads it, and DefaultMarketProvider is the union of those three keys.
Signing
signTypedData
Sign EIP-712 typed data with a private key. Useful for server-side signing without a wallet. Async — returnsPromise<Hex>.
signTypedDataWithSigner
Sign EIP-712 typed data with an externally-provided viemWalletClient — a browser wallet (wagmi), private key, or mnemonic. Use this to sign the user arm of a setup step with the end-user’s wallet. Async — returns Promise<Hex>.
Storage
Session credentials (provider agent keypairs, API keys, and read-only tokens) persist through a pluggableStorageAdapter — an async { get, set, remove } key-value interface. Pass a custom adapter through the provider/client config; the SDK ships two concrete adapters:
createMemoryStorage
In-memoryStorageAdapter, for testing or server-side use. State is lost when the process exits.
StorageAdapter.
localStorageAdapter
Default browserStorageAdapter: persists AES-GCM-256-encrypted values to localStorage, keyed by a non-extractable master key held in IndexedDB. Falls back to a no-op / null when localStorage, IndexedDB, or crypto.subtle is unavailable (e.g. server-side rendering) — it never writes plaintext. See Providers / Hyperliquid — Signing Model for the agent-keypair storage namespace built on this adapter.
Hyperliquid-specific
These utilities apply Hyperliquid’s order formatting and margin rules and are exported from@lifi/perps-sdk-provider-hyperliquid (not the core SDK). They are Hyperliquid-only: do not apply them to another provider’s markets. To snap an order price or size for any provider, use snapOrderPrice(sdk, market, price) / snapOrderSize(sdk, market, size), described in Provider plugin snapping.
calculateLiquidationPrice and calculateMaintenanceMarginRate return a DecimalString and throw PerpsError with ValidationError when the inputs cannot yield a valid result. safeCalculateLiquidationPrice and safeCalculateMaintenanceMarginRate give undefined instead.calculateMaintenanceMarginRate
Maintenance margin rate as a fraction:1 / (2 * maxLeverage). maxLeverage is a number. It throws when maxLeverage is not a finite number above zero.
calculateLiquidationPrice
Liquidation price for a new isolated position. It computesmmr as above. entryPrice and leverage are DecimalString values and maxLeverage is a number. It returns the same value as estimateLiquidationPrice({ entryPrice, leverage, isLong, maintenanceMarginRate: mmr }). For existing positions, prefer Position.liquidationPrice.
snapOrderPrice
Snap aDecimalString price onto the Hyperliquid tick grid. The price is rounded half-up to 6 - szDecimals decimal places (8 - szDecimals for 'spot'), then to 5 significant figures unless it is an integer. Trailing zeros are stripped and zero returns '0'.
snapOrderSize
Snap aDecimalString size onto the Hyperliquid lot grid. The size is truncated toward zero to szDecimals places and never rounded up. Trailing zeros are stripped.
getMaxPriceDecimals
Get the maximum number of price decimal places for an asset:6 - szDecimals for perps, 8 - szDecimals for spot (minimum 0).
positionRemovableMargin is documented under Margin adjustment capability. The Lighter and Ondo provider packages export their own snapOrderPrice(market, price), snapOrderSize(market, size), and positionRemovableMargin, with venue-specific rules.
Provider plugin snapping
Prices and sizes must sit on the venue’s tick and lot grid, or the order is rejected. Route every order price and size through the market’s own provider.snapOrderPrice(sdk, market, price) and snapOrderSize(sdk, market, size) take and give a DecimalString, and resolve the provider from market.providerId:
snapOrderPricerounds half-up onto the venue’s tick grid and returns a wire-readyDecimalString. It throwsPerpsErrorwithValidationErrorwhenmarketlacks the tick metadata the venue needs, and withSDKErrorwhenmarket.providerIdnames no registered provider.snapOrderSizetruncates toward zero to the venue’s lot grid (never rounds up) and returns aDecimalString.sizeis a non-negative magnitude.
perps.client.getProvider(market.providerId) returns the provider plugin (or undefined for an unregistered key). The plugin carries the same two methods, plus two more methods and one optional member:
estimateLiquidationPrice(market, { entryPrice, leverage, isLong })takesDecimalStringinputs and estimates the liquidation price of a new isolated position with the venue’s margin model. It returns aDecimalString, orundefinedwhen the model cannot be evaluated.positionRemovableMargin(position)is the method behindperps.getPositionRemovableMargin.triggerOrdersis the optional member with the TP/SL settings of the venue:modifyCanChangeType, and for LightermarketSlippage. See Provider trigger order settings for the fields and for an example that converts a slippage percent to thelimitPriceof a LighterMARKETleg withcalculateSlippagePriceandsnapOrderPrice.
Provider-Author Internals
The core SDK exports a small set of symbols intended for provider plugin authors, not consuming applications. They are documented here for completeness only.These symbols are exported from
@lifi/perps-sdk but are not part of the consumer-facing API. Building a UI or trading flow never requires them.parseStoredRecord, readValidatedRecord — parse and validate a record read from the configured storage adapter.
Transport: fetchWithRetry, resolveRetryPolicy, DISABLED_RETRY, LIFI_REQUEST_KEY, LIFI_RETRY_DEFAULTS — the retrying HTTP client a provider plugin uses to call its venue’s REST API.
Reference data registries: AssetRegistry, getAssetRegistry, MarketRegistry, getMarketRegistry, toMarketDisplay — per-provider hash-index caches over /assets and /markets. getAssetRegistry(client, provider) and getMarketRegistry(client, provider) return a stable instance per (client, provider) pair. Each registry exposes sync(), which fetches and dedupes concurrent in-flight calls (freshness follows the endpoint’s cache-control header rather than a fixed TTL), and get(id), an O(1) lookup that returns undefined and logs one warning for an unknown id. toMarketDisplay(market) projects a full Market down to MarketDisplay.
WebSocket base: WsProviderBase, ReconnectingWebSocket, cachePromise, WS_CHANNEL_TEARDOWN_LINGER_MS, wsLog — the reconnecting WebSocket transport and channel-subscription base class a provider plugin extends for streaming reads.
Quote resolution: resolveQuote, resolveQuoteMarket, resolveSubscribeQuote, QUOTE_THROTTLE_MS — the shared getQuote implementation a provider plugin calls to fulfill its own quote and quote-subscription methods.
Safe-function and venue-row helpers: createSafeFunction(name, fn) wraps a throwing function into a pair that logs one warning and gives undefined on a throw. createWarnOnce() gives a (key, message) logger that warns once per key. warnSkippedVenueRow(provider, row, field, value, { marketId?, expected? }) logs a deduplicated warning when a provider drops a venue row with a malformed field. timestampToIsoString(value) reads a venue time (epoch milliseconds or a date string) as an ISO-8601 string and throws PerpsError with ValidationError for an invalid time. safeTimestampToIsoString gives undefined instead.
Provider lookup: requireProvider(client, provider) gives the registered provider plugin and throws PerpsError with SDKError when the plugin is not registered.
WebSocket decoding: DecodeChain runs the asynchronous decode of each WebSocket frame in arrival order. DecodeChainMode is 'every' or 'latest': 'every' decodes every frame, and 'latest' replaces the frames that wait behind a running decode with the newest one. push(decode) queues a decode, reset() drops the queued one, and onError receives each decode failure.
Error and retry helpers: errorCodeFromStatus(status, fallback, statusCodes?) maps an HTTP status to a PerpsErrorCode (a 429 is always RateLimitExceeded; every other status takes fallback unless statusCodes overrides it). isAbortError(error) is true for an AbortError; transports rethrow aborts untouched instead of retrying or wrapping them. resolveRetryPolicy(base, config, providerKey) merges a RetryConfig onto a provider’s default policy: config[providerKey], then config.default, then base; a flat RetryPolicy applies to every provider and false disables retries.