Skip to content

Precision & Units ​

Most integration bugs are unit bugs. This page is the single reference — every other page links back here.

Quick table ​

FieldUnitExample
qtybase asset amount, 1e100.001 BTC → 10000000
price, stopLoss, takeProfit, triggerPrice, entryPriceUSD, 1e18$100,000 → 100000000000000000000000
amountIntokenIn decimals10 USDC → 10000000
extraFeetokenIn decimalssee Brokers & Referrals
margin, openFee, closeFee, holdingFee, fundingFee, pnllvToken decimalsLVUSD and LVMON are both 18
antiDdosFeefee token decimalssee Execution Fee
noncemilliseconds since epoch, uint641785312000123
deadlineseconds since epoch, uint481785312300
Percentage fields ending in P1e4 unless noted50 → 0.5%
holdingFeeRate1e12
fundingFeeRate, longAccFundingFeePerShare1e18, signed
shareP, minCloseFeeP, lvTokenDiscountP1e5

Unrealized PnL REST totals ​

The current unrealized PnL endpoint returns unrealizedPnlUsd, unrealizedPnlAfterAccruedFeesUsd, fundingFeeUsd and holdingFeeUsd as decimal USD strings, without an implied integer scale. "-12.5" means -$12.50. Do not pass these values to BigInt or divide them by 1e18; preserve the strings or use decimal arithmetic.

This differs from raw onchain amounts and the scaled integer strings in portfolio_data. The endpoint's blockNumber is an integer string; blockTimestamp and optional priceAsOf are Unix seconds.

qty is the trap ​

qty is not notional USD and not wei. It is the amount of the base asset, scaled by 1e10 — the same scale for every pair, regardless of the asset's own decimals.

To convert from a USD position size:

ts
import { parseUnits } from 'viem'

// $100 of BTC at $100,000
const notionalUsd = 100
const price = 100_000

const qty = parseUnits((notionalUsd / price).toFixed(10), 10)
// 0.001 BTC -> 10_000_000n

Going the other way, when reading a position:

ts
import { formatUnits } from 'viem'

const baseAmount = formatUnits(position.qty, 10)          // "0.001"
const notionalUsd =
  (Number(position.qty) / 1e10) * (Number(position.entryPrice) / 1e18)

WARNING

.toFixed(10) before parseUnits is not optional. 100 / 100000 in JavaScript is 0.001, but many other pairs produce values with more than 10 decimal places, and parseUnits throws on excess precision.

Prices are always 1e18 ​

Every price field in the protocol — the acceptable price on an open, stop-loss, take-profit, trigger prices on TP/SL orders, and the entry/close prices you read back — is USD at 18 decimals. This holds regardless of the collateral token's decimals.

ts
const price = parseUnits('100000', 18)  // $100,000

Amounts follow the token ​

amountIn is denominated in tokenIn's own decimals. USDC is 6; WMON, LVUSD and LVMON are 18. Read decimals() rather than assuming.

Values inside a position (margin, fees, PnL) are denominated in the lvToken, not tokenIn. A position funded with 10 USDC (amountIn at 6 decimals) reports its margin in LVUSD at 18 decimals — the two are not the same scale. See Collateral and lvToken.

Percentages ​

Fields whose name ends in P are integer percentages with an implied scale. The common case is 1e4, where 10000 = 100%:

ts
const slippagePercent = Number(config.slippageLongP) / 100  // 50 -> 0.5%

The exceptions are listed in the table above — shareP, minCloseFeeP and lvTokenDiscountP use 1e5, and holdingFeeRate uses 1e12.

Signed values ​

fundingFee, accruedFundingFee, pnl and fundingFeeRate are signed. A positive funding fee means the trader receives; negative means the trader pays. Decode them as int256/int96, not uint.

JSON transport ​

REST API responses and 1CT request bodies carry large integers as strings, not JSON numbers. nonce in particular exceeds Number.MAX_SAFE_INTEGER and will silently lose precision if you let JSON.stringify write it as a number.

ts
body: JSON.stringify({
  nonce: nonce.toString(),          // correct
  antiDdosFee: antiDdosFee.toString(),
})

Trading perpetuals involves risk. Nothing here is financial advice.