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:
| Path | Freshness | Answers with | Use when |
|---|---|---|---|
REST /token/{addr}/price | ~1 block (indexer lag) | One price, or null when unknown | One spot price in USD. |
REST /holdings/tokens | ~1 block | Prices for the curated set | Bulk price book in one call, with full-precision ethPrice. |
REST /tokens/{addr}/chart | ~1 block | Candles, empty for a token with no trading history | UI display; historical windows. |
RPC getReserves() | Immediate | Reserves on any V2-shaped pair | Pre-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"
}| Field | Type | Meaning |
|---|---|---|
priceUsd | string | null | Decimal-dollar string with up to six decimals. null for an unpriced or unknown token. |
source | string | null | Pricing source label. "onchain" for a pool-derived price. null when there is no price row. |
lastUpdated | string | null | ISO 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:
| Form | Example | Answer |
|---|---|---|
| Lowercase | 0xbd965230588eaa536de6aa45e8ebbc01638535e0 | 200 with the MOTO price |
| Checksummed | 0xBd965230588EAA536dE6aA45E8ebbc01638535e0 | 200 with the MOTO price |
| Uppercase hex | 0xBD965230588EAA536DE6AA45E8EBBC01638535E0 | 200 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" 13133693188290000nThe 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-beforenow − Wvia LOCF.- If the token is younger than the window,
ref_Wclamps 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 WETHThe 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, neverparseInt. Zero is"0", nevernull;priceUsdalone may benullwhen genuinely unpriced. ethPriceand every token amount are integer strings. Parse them withBigInt, neverNumber.getReserves(chain) returns raw base units asuint112.
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.