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.orgAPI 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.
| Surface | While launchPhase is dex |
|---|---|
/config | Every 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}/stats | Answer with data, from the Motoswap pairs the indexer tracks. |
/dex/pools, /dex/pools/{pair}, /tokens/{addr}/pools, swaps lists | Answer 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/supply | Answer with data. |
/analytics/*, /stats/series, /rakeback/*, /creator/*, /pve/*, /portfolio/{addr}/lp-fees | Answer 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
| Group | Purpose | Key endpoints |
|---|---|---|
| System | Liveness, build, config, chain head. | GET /health/live, GET /version, GET /config, GET /chain/head |
| Tokens | Directory (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 |
| MOTO | Where MOTO sits: staked by lock option, in pools. | GET /moto/overview |
| DEX | Pool 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 |
| Explore | Discovery lists sorted by cross-pool aggregates. | GET /explore/tokens, GET /explore/farms, GET /explore/creators |
| Analytics | Global 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 |
| Farms | All farm pools (bounded, unpaginated). | GET /farms |
| Stake | Lock option meta only. Per-user positions are chain-direct. | GET /stake/vaults |
| Portfolio | Per-user meta, LP fees, activity watermark. | GET /portfolio/{addr}, GET /portfolio/{addr}/lp-fees, GET /portfolio/{addr}/lp-fees/{pair}, GET /{addr}/meta |
| Holdings | User-agnostic token universe + price book. | GET /holdings/tokens |
| Activity | Global cursor-paginated event feed. | GET /activity |
| Swap | Cacheable candidate pair set for a route pair. | GET /swap/candidates?tokenIn=&tokenOut= |
| Points | Per-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} |
| Rakeback | Per-user accrual + merkle claim proofs. | GET /rakeback/stats, GET /rakeback/{addr}, GET /rakeback/{addr}/epoch/{epoch}, GET /rakeback/{addr}/proof |
| PnL | Per-user PnL, per token and per farm. | GET /pnl/{addr}/token/{token}, GET /pnl/{addr}/farm/{chef}/{pid} |
| Referrals | Per-user bindings + attribution. | GET /referrals/{addr}, POST /referrals/attribute |
| Motocats | Motocat ids held in a wallet. | GET /motocats/{addr} |
| Profiles | Public wallet profile (Motocat picture, X handle). | GET /profile/{addr}, GET /profiles?addresses= |
| Creator | Per-creator and per-token creator-fee accruals. | GET /creator/{addr}, GET /creator/token/{token} |
| PvE | PvE vault claims + per-token escrow. | GET /pve/wallet/{addr}, GET /pve/creator/{addr}, GET /pve/token/{token} |
| Launches | Launched-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.
range | Bucket | bucketMinutes | Rows |
|---|---|---|---|
24h | 1 minute | 1 | up to 1,441 |
7d | 15 minutes | 15 | up to 673 |
30d | 1 hour | 60 | up to 721 |
1y | 1 day | 1440 | up 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
| Domain | Runtime | Wire (JSON) | Example |
|---|---|---|---|
Address | `0x${string}` (lowercase) | string | "0xa0b8…eb48" |
TxHash | `0x${string}` | string | "0x…" |
Wei | bigint | decimal string | "1000000000000000000" |
Usd | bigint (µ-USD) | decimal-dollar string | "2500.50" |
Seconds | number | number | 1723456789 |
Percent | number | number | 10.5 |
*Bps | number | integer | 16261 (162.61%) |
- Parse
Weistrings withBigInt(value), neverNumber(value). - USD
0is"0", nevernull. Uncomputable USD (unknown price → 0) is also"0". - Truly-unknown metric-space fields (
priceUsd,aprBps, delta*Pct,decimals) staynullwhen uncomputable - rendering0would mislead. (Exception:/holdings/tokensserves a curated set whosedecimalsis 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:
| Tier | max-age | swr | Where |
|---|---|---|---|
HEARTBEAT | 1s | 9s | /chain/head |
BLOCK | 3s | 9s | Indexer-driven routes (incl. prices) |
CRON_FAST | 15s | 45s | /profiles |
CRON | 30s | 90s | /analytics/breakdown, /swap/candidates, /explore/creators, points leaderboard / weeks / leagues |
STATIC | 300s | 900s | /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. Seeguides/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": "..." } }| Status | Meaning |
|---|---|
400 | Bad request (validation failed, invalid address / range). |
403 | Tampered/replayed cursor, signature mismatch, forbidden. |
404 | Not found (unknown pair, unknown route). |
429 | Per-IP rate limit. Carries Retry-After. |
503 | Temporarily 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.