Pricing a token
If you only read one page, read this one. Every number below was read from Quai mainnet on 2026-09-23 (block 10,250,105) and checked against a simulated trade.
The short answer
Don't compute it — ask the API. GET https://hartiilabs.com/api/token/<address> returns the
indexer's price and market cap, already correct:
curl -s https://hartiilabs.com/api/token/0x0035187a7660f595d93cd53a4d16c635d6cffc8f| Field | Meaning |
|---|---|
token.lastPriceWei | Spot price in wei of QUAI per 1 whole token (divide by 1e18 for QUAI). |
token.totalSupplyWei | Live total supply: 1,000,000,000 minted − everything burned. |
token.marketCapWei | lastPriceWei × totalSupplyWei / 1e18, in wei of QUAI. |
token.burnedWei | Tokens burned all time (null = no burn indexed). |
All wei values are decimal strings — parse them with BigInt, never Number or parseInt.
If you want to compute it yourself from the chain (to avoid trusting us, or for a bot that must react inside a block), follow the rest of this page.
1. Spot price
A token is in one of two phases. Read curve.graduated() first (find the curve with
factory.curveOf(token)).
Bonding phase (graduated() == false) — the curve prices off virtual reserves:
quaiReserve = virtualQuaiReserve + realQuaiReserve
tokenReserve = virtualTokenReserve − tokensSold
price = quaiReserve / tokenReserve (QUAI per token)Graduated (graduated() == true) — the curve's own internal pool:
price = poolQuaiReserve / poolTokenReserve2. What a trade actually returns
quoteBuy and quoteSell do not apply the trading fee (feeBps, 100 = 1%). The real buy()
takes the fee off the QUAI you send before swapping; the real sell() takes it off the QUAI it
pays out.
buy: tokensOut = quoteBuy(value − value × feeBps / 10000)
sell: quaiOut = quoteSell(tokensIn) × (10000 − feeBps) / 10000Verified against a simulated buy() of exactly 1 QUAI:
| BFDQ (bonding phase) | QAXE (graduated) | |
|---|---|---|
feeBps | 100 | 100 |
| Spot price (QUAI per token) | 0.0000202571 | 0.0030421426 |
quoteBuy(1 QUAI) — wrong | 49,362.72 | 328.71 |
quoteBuy(0.99 QUAI) — correct | 48,869.12 | 325.43 |
Simulated buy() with 1 QUAI | 48,869.12 | 325.43 |
The correct formula matches the real trade to the wei; the naive quote overstates it by about 1%.
3. Market cap
marketCap = price × token.totalSupply()Read totalSupply() live from the token (or use the API's totalSupplyWei). Every Hartii token is
minted with exactly 1,000,000,000 and burns are the only thing that changes supply — verified on
all 33 mainnet tokens: totalSupply() == 1,000,000,000 − burned, to the wei.
| Live supply | Market cap (QUAI) | |
|---|---|---|
| BFDQ | 1,000,000,000 | 20,257 |
| QAXE | 877,074,085 (122.9M burned) | 2,668,184 |
Complete example (quais)
import { quais } from 'quais';
const provider = new quais.JsonRpcProvider('https://rpc.quai.network/cyprus1', undefined, { usePathing: false });
const addr = (a) => quais.getAddress(a.toLowerCase()); // the Quai RPC rejects lowercase addresses
const factory = new quais.Contract(addr('0x001AF1BbB40807fcb99C9Eeaa49dF5E91e7Efd42'),
['function curveOf(address) view returns (address)'], provider);
const CURVE_ABI = [
'function graduated() view returns (bool)', 'function feeBps() view returns (uint256)',
'function virtualQuaiReserve() view returns (uint256)', 'function realQuaiReserve() view returns (uint256)',
'function virtualTokenReserve() view returns (uint256)', 'function tokensSold() view returns (uint256)',
'function poolQuaiReserve() view returns (uint256)', 'function poolTokenReserve() view returns (uint256)',
'function quoteBuy(uint256) view returns (uint256)', 'function quoteSell(uint256) view returns (uint256)',
];
export async function priceToken(tokenAddress) {
const token = new quais.Contract(addr(tokenAddress), ['function totalSupply() view returns (uint256)'], provider);
const curve = new quais.Contract(await factory.curveOf(addr(tokenAddress)), CURVE_ABI, provider);
const graduated = await curve.graduated();
const [qr, tr] = graduated
? await Promise.all([curve.poolQuaiReserve(), curve.poolTokenReserve()])
: await Promise.all([
Promise.all([curve.virtualQuaiReserve(), curve.realQuaiReserve()]).then(([v, r]) => v + r),
Promise.all([curve.virtualTokenReserve(), curve.tokensSold()]).then(([v, s]) => v - s),
]);
const priceWei = (qr * 10n ** 18n) / tr; // wei of QUAI per whole token
const supply = await token.totalSupply();
const marketCapWei = (priceWei * supply) / 10n ** 18n;
const feeBps = await curve.feeBps();
const quoteBuy = (quaiWei) => curve.quoteBuy(quaiWei - (quaiWei * feeBps) / 10000n);
const quoteSell = async (tokensWei) => ((await curve.quoteSell(tokensWei)) * (10000n - feeBps)) / 10000n;
return { graduated, priceWei, supply, marketCapWei, quoteBuy, quoteSell };
}When you actually trade, pass a slippage floor built from the fee-correct quote — e.g.
minTokensOut = quote × 99 / 100 — to buy(minTokensOut).
The three mistakes everyone makes
- Quoting with
quoteBuy(value)— overstates by the 1% fee. Take the fee off first. - Estimating supply from
curveSupply(e.g.curveSupply × 10 / 8) — wrong whenever a token has a creator allocation or any burns. For QAXE it overstated market cap by about 12%; for POEM (344M burned) by about 53%. UsetotalSupply()/totalSupplyWei. - Pricing from the curve's QUAI balance — includes unwithdrawn fees. Use the reserve getters.
Around graduation
At the graduating trade the pool is seeded from the curve's real reserves only (virtual liquidity does not carry over), so the price after graduation can differ from the price just before. This is deliberate — see Bonding curve › Graduation mechanics.
QAXE's second market
QAXE also trades on a standard Quainance V2 pair. Its price is reserveWQUAI / reserveQAXE and its
fee is 0.3%; arbitrage keeps it aligned with the curve pool (0.001% apart when checked). See
QAXE markets.