Documentation
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#
| Symbol | Address | Decimals |
|---|---|---|
| USDC | 0x3600000000000000000000000000000000000000 | 6 |
| EURC | 0xbef5f6d51cb62b58e6a8f77868681825c6fe21c1 | 6 |
| cirBTC | 0x171a4217b86a807a64eb94757db6849fb4bdbaa0 | 8 |
| WETH | 0x128cc466b61f542da60c70e3aa11c10e19b84edb | 18 |
| XAUM | 0x178b01f61cbea1d2a5581fe1621be607835ec349 | 18 |
The live list — with each asset's phantom reserve and graduation threshold — is
GET /v1/protocol → approvedPairTokens[], 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#
| Quantity | Decimals |
|---|---|
quoteIn, minQuoteOut, phantomQuote, graduationThreshold, escrow balances, distributor epochs | the launch's quote asset (6, 8 or 18 — table above) |
launchFee | the fee token's — launchFeeToken() is USDC, so 6, whatever the launch quotes in |
minTokensOut, tokensIn, supply, token balances, curve token reserves | 18 |
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.
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 6The 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:
balance 714275814275814275814275815
via a float 714275814275814175605773926
lost 100208501889 weiThat 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.
const memecoinIsCurrency0 = token.toLowerCase() < pairToken.toLowerCase();
const currency0 = memecoinIsCurrency0 ? token : pairToken;
const currency1 = memecoinIsCurrency0 ? pairToken : token;