Create
Documentation
Guides

Decimals

Every launch quotes in one of the approved pair tokens, and every quote-side amount is in that asset's own decimals. Every launched memecoin is 18 decimals. Almost every arithmetic bug in an integration comes from mixing the two — or from assuming the quote is always the 6-decimal one.

The quote assets#

SymbolAddressDecimals
USDC0x36000000000000000000000000000000000000006
EURC0xbef5f6d51cb62b58e6a8f77868681825c6fe21c16
cirBTC0x171a4217b86a807a64eb94757db6849fb4bdbaa08
WETH0x128cc466b61f542da60c70e3aa11c10e19b84edb18
XAUM0x178b01f61cbea1d2a5581fe1621be607835ec34918

The live list — with each asset's phantom reserve and graduation threshold — is GET /v1/protocolapprovedPairTokens[], and on chain factory.pairTokenEconomics(token) plus the token's own decimals(). Read it per launch: getLaunchedToken(token).pairToken says which asset a given curve trades in.

Which side is which#

QuantityDecimals
quoteIn, minQuoteOut, phantomQuote, graduationThreshold, escrow balances, distributor epochsthe launch's quote asset (6, 8 or 18 — table above)
launchFeethe fee token'slaunchFeeToken() is USDC, so 6, whatever the launch quotes in
minTokensOut, tokensIn, supply, token balances, curve token reserves18

So on a USDC launch 5e6 is five dollars and 5e18 is five tokens; on a WETH launch a quoteIn of 5e6 is 0.000000000005 ETH — dust. The 5 USDC launch fee reads back from launchFee() as 5000000 for every launch.

ts
const { pairToken } = await client.readContract({ address: FACTORY, abi: factoryAbi, functionName: "getLaunchedToken", args: [token] });
const quoteDecimals = await client.readContract({ address: pairToken, abi: erc20Abi, functionName: "decimals" });
const quoteIn = parseUnits("0.05", quoteDecimals);   // never a hard-coded 6

The native asset is a different thing#

Arc uses USDC for gas, and viem's chain definition describes that native asset as { symbol: "USDC", decimals: 18 }. That is not the ERC-20 you trade with. They report the same balance through two different interfaces at two different scales. Reading the native balance and passing it as quoteIn is a 10^12 error.

Always read the traded balance from the ERC-20 at 0x3600…0000 — and, for any other quote, from that quote's own contract.

Never round-trip through a float#

Number(raw) / 1e18 loses precision above about 2^53. On a real balance the loss is large enough to matter:

code
balance          714275814275814275814275815
via a float      714275814275814175605773926
lost                         100208501889 wei

That is a hundred billion wei of dust that a "sell max" can never reach, because the amount submitted was never quite the amount held. Use formatUnits and parseUnits, which are exact inverses, and keep bigints end to end.

sqrtPriceX96 carries the skew#

The graduated pool's sqrtPriceX96 is derived from raw amount ratios, so it embeds the decimal gap between the quote side and the 18-decimal memecoin — 10^12 against USDC or EURC, 10^10 against cirBTC, none against WETH or XAUM. It is not a human price. Adjust for the launch's quote decimals before displaying anything derived from it.

Currency ordering is by address, not by role#

A V4 PoolKey sorts its two currencies by raw address. The quote asset is currency0 only when the memecoin's address happens to sort above it — for USDC (0x36…) that is most of the time, for WETH (0x12…) less often, and never reliably, because launch addresses are CREATE2 and effectively random.

Compute the ordering per launch. Assuming the quote is always currency0 will silently invert zeroForOne on some tokens and swap the wrong direction.

ts
const memecoinIsCurrency0 = token.toLowerCase() < pairToken.toLowerCase();
const currency0 = memecoinIsCurrency0 ? token : pairToken;
const currency1 = memecoinIsCurrency0 ? pairToken : token;