REST API

The Motoswap REST API - endpoint groups, wire types, caching, pagination.

The Motoswap REST API serves indexed reads for Ethereum mainnet (chain id 1). Almost every route is a GET. The few writes are wallet-signed (POST /referrals/attribute, the profile routes). Base URL:

https://api.motoswap.org

API host note: api.motoswap.org is the permanent public API host and the only one to build against. It answers for chain id 1. Do not substitute any other hostname you may find: temporary hosts get removed.

Full endpoint reference: reference/api-full-inventory.

Which routes carry data

Deployment status is per contract. The status table is on the contract addresses page, and GET /config is the machine-readable version: a key holds the zero address while its contract is unset for the current phase. Read every address from /config and never integrate against one it did not give you.

Every route answers 200 with its final shape regardless. What differs is whether there is data behind it. GET /config reports the phase as launchPhase. It reads dex with the DEX deployed and open, and it read vamp before the DEX opened.

SurfaceWhile launchPhase is dex
/configEvery deployed key carries an address, and launchPhase reads dex. A key that is unset for the phase reads the zero address. Read addresses from /config, never hardcode.
/tokens, /token/{addr}/price, /holdings/tokens, /tokens/{addr}/statsAnswer with data, from the Motoswap pairs the indexer tracks.
/dex/pools, /dex/pools/{pair}, /tokens/{addr}/pools, swaps listsAnswer with data for the Motoswap pairs.
/farms, /explore/farms, /pnl/{addr}/farm/{chef}/{pid}Answer with data for the farms the chef in /config owns.
/stake/vaults, /moto/overview, /tokens/moto/supplyAnswer with data.
/analytics/*, /stats/series, /rakeback/*, /creator/*, /pve/*, /portfolio/{addr}/lp-feesAnswer with data. Each one is driven by the protocol fee, so a wallet or token with no fee-paying trades gets an empty answer rather than an error.
/points/*, /referrals/{addr}Answer with the wallet's balances and the leaderboard.

Every empty answer has the same shape as a populated one, so you can build and test parsers against it.

Responses can carry fields that are not listed here. Ignore fields you do not recognise; do not build a parser that rejects them.

Endpoint groups

GroupPurposeKey endpoints
SystemLiveness, build, config, chain head.GET /health/live, GET /version, GET /config, GET /chain/head
TokensDirectory (with the verified curation flag), price, chart, per-token pools/swaps, MOTO supply.GET /tokens, GET /token/{addr}/price, GET /tokens/{addr}/chart, GET /tokens/{addr}/stats, GET /tokens/{addr}/swaps, GET /tokens/{addr}/pools, GET /tokens/moto/supply
MOTOWhere MOTO sits: staked by lock option, in pools.GET /moto/overview
DEXPool list / detail / chart / holder.GET /dex/pools, GET /dex/pools/{pair}, GET /dex/pools/{pair}/chart, GET /dex/pools/{pair}/swaps, GET /dex/pools/holder/{addr}/pairs
ExploreDiscovery lists sorted by cross-pool aggregates.GET /explore/tokens, GET /explore/farms, GET /explore/creators
AnalyticsGlobal totals, breakdowns, series.GET /analytics/overview, GET /analytics/breakdown, GET /analytics/revenue/series, GET /analytics/revshare/series, GET /analytics/creator-fees/series, GET /stats/series
FarmsAll farm pools (bounded, unpaginated).GET /farms
StakeLock option meta only. Per-user positions are chain-direct.GET /stake/vaults
PortfolioPer-user meta, LP fees, activity watermark.GET /portfolio/{addr}, GET /portfolio/{addr}/lp-fees, GET /portfolio/{addr}/lp-fees/{pair}, GET /{addr}/meta
HoldingsUser-agnostic token universe + price book.GET /holdings/tokens
ActivityGlobal cursor-paginated event feed.GET /activity
SwapCacheable candidate pair set for a route pair.GET /swap/candidates?tokenIn=&tokenOut=
PointsPer-user points, leaderboard, public season config.GET /points/{addr}, GET /points/leaderboard, GET /points/weeks, GET /points/leagues, GET /points/rules, GET /points/fees, GET /points/pair/{pair}
RakebackPer-user accrual + merkle claim proofs.GET /rakeback/stats, GET /rakeback/{addr}, GET /rakeback/{addr}/epoch/{epoch}, GET /rakeback/{addr}/proof
PnLPer-user PnL, per token and per farm.GET /pnl/{addr}/token/{token}, GET /pnl/{addr}/farm/{chef}/{pid}
ReferralsPer-user bindings + attribution.GET /referrals/{addr}, POST /referrals/attribute
MotocatsMotocat ids held in a wallet.GET /motocats/{addr}
ProfilesPublic wallet profile (Motocat picture, X handle).GET /profile/{addr}, GET /profiles?addresses=
CreatorPer-creator and per-token creator-fee accruals.GET /creator/{addr}, GET /creator/token/{token}
PvEPvE vault claims + per-token escrow.GET /pve/wallet/{addr}, GET /pve/creator/{addr}, GET /pve/token/{token}
LaunchesLaunched-token metadata and logos.GET /launchpad/recent, GET /launchpad/token/{addr}, GET /launchpad/token/{addr}/logo

A farm position is addressed by chef and pid: GET /pnl/{addr}/farm/{chef}/{pid}. The older /pnl/{addr}/farm/{pid} form answers 404. Take chef from the pool's row in GET /farms.

Pool chart ranges

GET /dex/pools/{pair}/chart takes range, one of 24h, 7d, 30d or 1y, defaulting to 24h. Anything else fails validation. Bucket width follows the range, so a long window stays a bounded response rather than a year of minutes. The width is echoed back as bucketMinutes.

rangeBucketbucketMinutesRows
24h1 minute1up to 1,441
7d15 minutes15up to 673
30d1 hour60up to 721
1y1 day1440up to 366

Every range arrives whole. Rows are sparse: a bucket with no trade has no candle.

Candles are OHLC only. minute is a number, the bucket's start in unix minutes, not seconds, so multiply by 60 for a timestamp. Read the spacing off bucketMinutes rather than assuming it.

{
  "bucketMinutes": 1,
  "candles": [
    { "minute": 29828778, "open": "2.4", "high": "2.6", "low": "2.3", "close": "2.5" },
    { "minute": 29828779, "open": "2.5", "high": "2.7", "low": "2.5", "close": "2.6" }
  ]
}

The per-token chart, GET /tokens/{addr}/chart, is a different route with its own frames. These four ranges are the pool chart only.

Non-public routes (do not integrate against)

The backend also mounts operational routes: deep health checks, scheduled jobs, admin controls, an indexer webhook and the web app's own RPC proxy. They are token-gated or rate-limited for the app, and they change without notice. Build only against the routes listed on this page and in the full inventory.

Wire types cheat sheet

DomainRuntimeWire (JSON)Example
Address`0x${string}` (lowercase)string"0xa0b8…eb48"
TxHash`0x${string}`string"0x…"
Weibigintdecimal string"1000000000000000000"
Usdbigint (µ-USD)decimal-dollar string"2500.50"
Secondsnumbernumber1723456789
Percentnumbernumber10.5
*Bpsnumberinteger16261 (162.61%)
  • Parse Wei strings with BigInt(value), never Number(value).
  • USD 0 is "0", never null. Uncomputable USD (unknown price → 0) is also "0".
  • Truly-unknown metric-space fields (priceUsd, aprBps, delta *Pct, decimals) stay null when uncomputable - rendering 0 would mislead. (Exception: /holdings/tokens serves a curated set whose decimals is always known, so that field is non-nullable there.)

Caching / ETag semantics

Cached GETs emit Cache-Control and a weak ETag. Send the tag back verbatim in If-None-Match and expect 304. /config carries Cache-Control but no ETag. /health/live, /version and every write are private, no-store. Tiers:

Tiermax-ageswrWhere
HEARTBEAT1s9s/chain/head
BLOCK3s9sIndexer-driven routes (incl. prices)
CRON_FAST15s45s/profiles
CRON30s90s/analytics/breakdown, /swap/candidates, /explore/creators, points leaderboard / weeks / leagues
STATIC300s900s/config, /points/rules, /rakeback/stats, past-epoch Rakeback, Rakeback proofs

The header is public, max-age=N, s-maxage=N, stale-while-revalidate=M. Per-user routes key on the wallet's last_activity_block, so polling is free until the user transacts. See guides/caching-and-etags.

Pagination

Cursor lists return `{ items, nextCursor, hasMore }` (/explore/farms names its array farms, /explore/creators names it creators). The cursor is an HMAC-signed token bound to the endpoint

  • filters. Cannot be replayed across queries. The per-token and per-pool swap and pool lists take ?page= instead. See guides/cursor-pagination.

Error envelope

Handler errors:

{ "error": "message string" }

A path or query value that fails schema validation (a malformed address, for example) can answer with the validator's own shape instead. Branch on the status code, not the body:

{ "success": false, "error": { "name": "ZodError", "message": "..." } }
StatusMeaning
400Bad request (validation failed, invalid address / range).
403Tampered/replayed cursor, signature mismatch, forbidden.
404Not found (unknown pair, unknown route).
429Per-IP rate limit. Carries Retry-After.
503Temporarily degraded (indexer lag, missing config, upstream).

4xx and 5xx responses are never cached (middleware overrides Cache-Control to no-store).

CORS

The API's CORS policy is set for the Motoswap app origin. From another origin, a plain GET with no custom headers is readable in a browser, but any request the browser preflights is blocked: the preflight answer carries no Access-Control-Allow-Origin for your origin. That covers a fetch where your script sets If-None-Match, and every JSON POST. Conditional polling with an explicit If-None-Match must run server-side, or rely on the browser's own HTTP cache, which revalidates without a preflight.

Rate limiting

Routes are rate-limited per IP, and under heavy load the backend sheds requests. Both refusals are 429 or 503 carrying Retry-After. Honor the header and back off. In steady state you should never see it - ETag caching keeps polls near zero.

On this page