Guides

Read Prices

Four ways to read token prices: one token over REST, the bulk price book, the chart route, or reserves on chain.

Four ways, each with a specific niche:

PathFreshnessAnswers withUse when
REST /token/{addr}/price~1 block (indexer lag)One price, or null when unknownOne spot price in USD.
REST /holdings/tokens~1 blockPrices for the curated setBulk price book in one call, with full-precision ethPrice.
REST /tokens/{addr}/chart~1 blockCandles, empty for a token with no trading historyUI display; historical windows.
RPC getReserves()ImmediateReserves on any V2-shaped pairPre-submit sanity; quoting a specific pool.

REST prices come from the pairs the indexer tracks, Motoswap and Uniswap V2 both. Per-contract deployment status is on the contract addresses page.

The API's CORS policy is set for the Motoswap app origin. From a browser on another origin a plain GET works, but a fetch where your script sets If-None-Match is preflighted and blocked. Run the ETag examples on this page server-side.

Option A: REST, per token

Mind the path. The price route is singular, /token/{address}/price. The chart route in Option C is plural, /tokens/{address}/chart. Swap them and you get a 404: /tokens/{address}/price and /token/{address}/chart do not exist.

This runs as written and prints the MOTO price:

const MOTO = "0xbd965230588eaa536de6aa45e8ebbc01638535e0";

const r = await fetch(`https://api.motoswap.org/token/${MOTO}/price`);
const { priceUsd, source } = await r.json();
console.log(priceUsd, source); // "0.003295" "onchain"

The full response:

{
  "priceUsd": "0.003295",
  "source": "onchain",
  "lastUpdated": "YYYY-MM-DDTHH:mm:ss.sssZ"
}
FieldTypeMeaning
priceUsdstring | nullDecimal-dollar string with up to six decimals. null for an unpriced or unknown token.
sourcestring | nullPricing source label. "onchain" for a pool-derived price. null when there is no price row.
lastUpdatedstring | nullISO 8601 UTC timestamp with milliseconds, from the token's price row. It can trail the price itself, so do not use it as a freshness signal. Use GET /chain/head for that.

An unknown token answers 200 with all three fields null, not 404. For a full-precision price use ethPrice from Option B.

Address case does not matter on this route. Tested with the MOTO address in three forms:

FormExampleAnswer
Lowercase0xbd965230588eaa536de6aa45e8ebbc01638535e0200 with the MOTO price
Checksummed0xBd965230588EAA536dE6aA45E8ebbc01638535e0200 with the MOTO price
Uppercase hex0xBD965230588EAA536DE6AA45E8EBBC01638535E0200 with the MOTO price

The route does not verify the checksum. An address of the wrong length, or one without the 0x prefix, answers 400. Each spelling is a separate cache entry at the edge, so pick one form (lowercase matches what the API returns everywhere else) and keep to it.

The response carries Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 and a weak ETag.

Option B: REST, bulk price book

const MOTO = "0xbd965230588eaa536de6aa45e8ebbc01638535e0";

const book = await (await fetch("https://api.motoswap.org/holdings/tokens")).json();
// book.tokens[].{address, decimals, priceUsd, ethPrice, priceUsd24hAgo}
const moto = book.tokens.find((t: { address: string }) => t.address === MOTO);

// Value 10,000 MOTO in WETH wei with integer math only.
const amount = 10_000n * 10n ** BigInt(moto.decimals);
const valueWethWei = (amount * BigInt(moto.ethPrice)) / 10n ** BigInt(moto.decimals);
console.log(moto.priceUsd, valueWethWei); // "0.003296" 13133693188290000n

The shape is strict: address, decimals, the two USD prices, and ethPrice (WETH per whole token scaled by 1e18, an integer string, nullable). There is no symbol or name (resolve those from GET /tokens). Best when you have a wallet's balances and want to value them all at once. On this endpoint decimals is always a number (the set is curated), and for precision-critical valuation of low-priced tokens prefer ethPrice x your WETH/USD anchor over the priceUsd, which is cut to six decimal places. priceUsd24hAgo is null on every row until candles exist.

Option C: REST chart

const r = await fetch(
  `https://api.motoswap.org/tokens/${tokenAddress}/chart?range=24h`,
  { headers: savedEtag ? { "If-None-Match": savedEtag } : {} },
);

if (r.status === 304) {
  // cache hit - reuse your saved data
} else {
  const data = await r.json();
  savedEtag = r.headers.get("etag");
  console.log(data.price);              // decimal-dollar string, or null
  console.log(data.change24hPct);       // number, e.g. 5.0, or null
  console.log(data.volume24hUsd);       // decimal-dollar string (or "0")
  console.log(data.candles);            // array of OHLC candles
}

Candles are built from Motoswap pair trades, so a token with no Motoswap trading history answers price: null, null deltas, "0" volumes and candles: []. Use Option A or B for a spot price.

Range values: the long frames 24h | 7d | 30d (buckets 1min / 30min / 120min) plus the short frames 1m | 5m | 10m | 30m, named for their candle interval (windows 6h / 12h / 24h / 3d). The bucket is echoed back as bucketMinutes.

Each candle looks like this:

{
  "minute": 29762621,
  "open":  "0.0003459298143979567603005925",
  "high":  "0.0003459298143979567603005925",
  "low":   "0.0003454649992475997730919503",
  "close": "0.0003454649992475997730919503",
  "volumeUsd": "199.325593"
}

minute is a bucket index, not a timestamp. Multiply by 60 to get Unix seconds for your x-axis:

const unixSeconds = candle.minute * 60;
const date = new Date(unixSeconds * 1000);

Price-change formula (change_W = (spot − ref_W) / ref_W × 100):

  • ref_W = last candle close at-or-before now − W via LOCF.
  • If the token is younger than the window, ref_W clamps to the FIRST priced tick (the launch price), not the creator's first buy.
  • If the last trade is older than the window → null (rendered as a dash), not stale.

Option D: read reserves on chain (live)

The on-chain MOTO/WETH pair for the current phase is the one GET /config serves as motoUniswapPair, and that is what this example reads. It runs as written with your own mainnet RPC endpoint in RPC_URL:

import { createPublicClient, http, parseAbi } from "viem";

const API = "https://api.motoswap.org";
const RPC_URL = process.env.RPC_URL!; // your own Ethereum mainnet RPC endpoint

const { addresses } = await (await fetch(`${API}/config`)).json();
const client = createPublicClient({ transport: http(RPC_URL) });

const pairAbi = parseAbi([
  "function getReserves() view returns (uint112, uint112, uint32)",
  "function token0() view returns (address)",
]);

const pair = addresses.motoUniswapPair;
const [r0, r1] = await client.readContract({ address: pair, abi: pairAbi, functionName: "getReserves" });
const token0 = await client.readContract({ address: pair, abi: pairAbi, functionName: "token0" });

const motoIsToken0 = token0.toLowerCase() === addresses.motoToken;
const motoReserve = motoIsToken0 ? r0 : r1;
const wethReserve = motoIsToken0 ? r1 : r0;

// WETH per whole MOTO, scaled by 1e18. Both tokens have 18 decimals.
const ethPerMoto = (wethReserve * 10n ** 18n) / motoReserve;
console.log(ethPerMoto); // e.g. 1314427199219n = 0.000001314 WETH

The same read works on any Motoswap pair. Get the pair from factory.getPair(tokenA, tokenB), with factory taken from GET /config. A zero factory there is unset for the phase, so do not call it.

The SDK specifies the same math as priceFromReserves (against a stablecoin side) and bestRoute (multi-hop over the candidate set from GET /swap/candidates, see reference/sdk-full-inventory). The SDK is not published yet, so port the formulas from that page.

Wire types & precision

  • USD values (REST) are decimal-dollar STRINGS (e.g. "394066.852165") - parse as a decimal, never parseInt. Zero is "0", never null; priceUsd alone may be null when genuinely unpriced.
  • ethPrice and every token amount are integer strings. Parse them with BigInt, never Number.
  • getReserves (chain) returns raw base units as uint112.

When to use which

UI

REST, through your server. Cache the ETag; polls collapse to 304 (free).

Pre-submit slippage check

Chain-direct. Reserves may have moved since the quote you presented; re-price live before signing.

Batch valuation

REST /holdings/tokens (one call, returns the whole curated set).

Aggregator / MEV bot

Always chain-direct via multicall; REST is not meant for hot-loop pricing.

Why the API doesn't drift

Both the REST API and the chain-direct path read the same reserves. The API's freshness lag is about one block: the REST value is a snapshot of the reserves your live getReserves would have returned a block earlier.

On this page