REST API
The two HTTP surfaces for moto.fun. The Motoswap API is the one to integrate against, and the app's own backend carries the rest.
moto.fun data is served by two different services. Pick deliberately.
| Motoswap API | The moto.fun app backend | |
|---|---|---|
| Base | https://api.motoswap.org | <motofunUrl>/backend-fun/<chain-slug> |
In /openapi.json | Yes | No |
ETags / 304 | Yes, every route | Yes, on JSON 200 responses |
| Pagination | Cursor, { items, nextCursor, hasMore } | Signed cursors on the board and the graduated feed. Hardcoded caps everywhere else |
| Realtime | No | Server-Sent Events |
| Candles | No | Yes |
| Auth | None | None on reads |
| Use it for | Discovery, listings, venue stats, revenue | Charts, holders, pending launches, streams |
Full endpoint reference: moto.fun API inventory.
Integrate against the Motoswap API first
api.motoswap.org is the permanent public host and the documented surface, and its /motofun/*
routes are in /openapi.json. Reach for the app backend only for
what the Motoswap API does not carry.
Motoswap API: the /motofun group
| Endpoint | Purpose |
|---|---|
GET /motofun/stats | Venue counters: coins by lifecycle, TVL, 24h and all-time volume, fees split protocol vs creator, trades, launches |
GET /motofun/coins?cursor= | Newest-first coin list, 25 per page, with status, both pair addresses, volume, trade count, last price |
GET /motofun/coins/{address} | Point lookup. "Is this a curve coin, and has it graduated?" |
GET /motofun/revenue/series?range=&bucket= | Per-bucket venue revenue, Collector leg and creator leg separately |
Plus five that carry moto.fun without being under the prefix:
| Endpoint | moto.fun content |
|---|---|
GET /config | addresses.launchpadCurve, addresses.launchpadLens, motofunEnabled, motofunUrl |
GET /activity?venue=motofun | The unified feed, filtered to motofun_launch, motofun_trade, motofun_graduation |
GET /analytics/overview | An optional top-level motofun: { volumeUsd, feesUsd }, absent when moto.fun is off |
GET /creator/{address} | Per-coin creator earnings, each tagged venue: "motofun" | "deployer". "deployer" marks coins from the retired launch product |
GET /launchpad/token/{address}/logo | A coin's logo as bytes, mirrored from moto.fun |
Semantics that will bite you
- A
404usually means "moto.fun is off on this deploy", not "no data". Every/motofun/*route answers404 {"error":"moto.fun is not enabled on this deploy"}when the curve address is zero. Check/configfirst and you will never see that one. The point lookup has a second404of its own: with the venue on,GET /motofun/coins/{address}answers404 {"error":"this token never launched on the moto.fun curve"}for any address that is not a curve coin. That is the ordinary answer for an ordinary ERC-20, so tell the two apart by the body, or by readingmotofunEnabledfirst. statusis folded on read, from the latest surviving lifecycle event. A coin with no lifecycle row is"bonding". There is no stored status column, deliberately, so a reorg is a range delete with nothing to rebuild.- USD fields are frozen write-time snapshots, never re-priced, except
tvlUsd, which is a live gauge. A trade recorded at one ETH price keeps that dollar value forever. - The 24h window is anchored to the indexer head block timestamp, not to wall clock.
- A malformed cursor returns an empty page with
200, not a403. Do not read an empty page as "end of list" without checkinghasMore. /motofun/*ETags version on the curve's own head block, so they stay304through DEX-only blocks. Poll them freely withIf-None-Match.
Wire types
Same conventions as the rest of the Motoswap API: addresses lowercase, wei and uint256 as decimal
strings, USD as decimal-dollar strings, seconds as numbers. See the
API wire types cheat sheet. Rate limit on /motofun/* is per-IP
and generous; /launchpad/* is tighter because the logo route serves bytes.
The app backend
Everything below is under <motofunUrl>/backend-fun/<chain-slug>/. No auth on reads,
{"error": "..."} on failure. ETags are served, on JSON 200 responses only.
Where this is reachable
The public host is motofunUrl from GET https://api.motoswap.org/config. Read it there rather than pinning one.
Pre-release environments are access-controlled and answer 401 without credentials. Everything on
chain is readable without any.
| Endpoint | Purpose |
|---|---|
GET /config | Curve, lens, token implementation, PvE vault, WETH, MOTO, the EIP-712 domain |
GET /health | Indexer lag, frozen coins, keeper state, PvE failures. Operational, not a data source |
GET /health/live | Liveness only: { ok, commit }. Poll this rather than /health |
GET /version | Which build is serving right now: { commit, builtAt, chainId } |
GET /status | The public three-field health answer: state, a short checks list, and an operator notice (an object or null). Use this, not /health, to tell an outage from your own network |
GET /tokens?limit=&cursor=&q= | The board: coins plus pending intents plus the ETH/USD rate. Paginated, see below |
GET /tokens/{address} | One coin, with a live curve read (including its paramsId), recent trades, stats, and PvE totals |
GET /tokens/{address}/quote?side=&amount= | A lens quote served from the backend. side is buy or sell, amount is wei of ETH in or of coin in |
GET /tokens/{address}/snapshot | The coin page in one body: the detail, the holders, and optionally candles and a quote, in the same shapes as the separate routes |
GET /graduated/feed?limit=&cursor= | Graduated coins, newest graduation first, with both pair addresses. The feed for listing sites |
GET /tokens/{address}/candles?resolution=&from= | OHLCV buckets. See realtime |
GET /tokens/{address}/holders | Top 10 net holders, the true holder count, and the PvE escrow balance |
GET /tokens/{address}/image | The creator's picture as bytes, re-verified against the on-chain hash |
GET /tokens/{address}/card.png | A 1200x630 share card |
GET /share/{address} | The crawler unfurl page. HTML, and text/plain on error |
GET /trades/recent | 20 newest trades and 6 newest launches from the last 24h |
GET /stats | Coin counts, volume, creator fees, PvE totals |
GET /creators/top | Top 25 creators by fees earned, folded per creator |
GET /pve/value | What the PvE escrow is currently worth on this chain |
GET /wallet/{address}/positions | Net positions with cost basis. Rate-limited |
GET /wallet/{address}/trades-today | The wallet's trades since 00:00 UTC today. Rate-limited |
GET /wallet/{address}/fee-reroutes | Coins whose creator fee failed over to the Collector |
GET /fees/history/{wallet} | Lifetime creator-fee claims. Rate-limited. Carries no live balances |
GET /intents/precheck?creator=&symbol= | Pre-signature advisory: throttled, reserved, slots free |
POST /intents | Submit a signed LaunchIntent. See below |
GET /intents/{address} | A pending launch's signed payload, verbatim |
GET /stream | Server-Sent Events. See realtime |
Not for integration: GET /tickers/{symbol}/available is deprecated and always answers true. The
app also calls a handful of routes for its own sign-in and reporting flows (/terms/*,
/screening/{address}, /geo/gate, POST /reports). They exist for the app, their shapes are not
documented, and they can change without notice. There
is also an operator-gated /admin/* group covering moderation, the audit log, metadata overrides and
bans. It answers 401 without a credential, it is no-store throughout, and its shapes are
deliberately not documented here. Do not build against it.
Caching and ETags
Every route carries a Cache-Control value chosen per route, and JSON 200 responses also carry a
weak ETag. Send If-None-Match and you get a 304 with no body, keeping the same Cache-Control
and the same ETag.
| Tier | Routes |
|---|---|
no-store | /health, /health/live, /version, /intents/precheck, POST /intents, /stream, every /admin/*, and all four wallet money routes |
no-cache (store, revalidate every time) | /config, /intents/{address}, /tokens, /tokens/{address}, its candles and holders, /trades/recent, /stats |
public, max-age=10, stale-while-revalidate=20 | /creators/top |
public, max-age=30, stale-while-revalidate=30 | /pve/value |
public, max-age=300, stale-while-revalidate=600 | /share/{address} |
public, max-age=300 | /tickers/{symbol}/available |
| Set by the handler | /tokens/{address}/image (max-age=86400 on the CDN redirect, max-age=300 inline) and /tokens/{address}/card.png (max-age=30) |
Three things follow from this that will catch you out. no-cache does not mean "do not cache", it
means "store it and revalidate before every reuse", which is the tier most of this API wants. The
wallet money routes are no-store rather than no-cache on purpose, so a balance cannot be read
back off a shared cache. And there are no ETags on redirects, on the PNG routes, or on any non-200,
so do not write a client that expects one everywhere.
Rate limits
Per-IP token buckets, each over a 10-minute window with continuous refill. The caps below are the defaults and every one is env-configurable, so treat them as the shipping values rather than as a contract.
| Bucket | Env var | Default | Applies to |
|---|---|---|---|
| Intent writes | INTENTS_PER_IP_PER_10MIN | 5 | POST /intents |
| Board | BOARD_READS_PER_IP_PER_10MIN | 6000 | GET /tokens |
| Coin detail | TOKEN_DETAIL_READS_PER_IP_PER_10MIN | 30000 | GET /tokens/{address}, and one token per snapshot |
| Quotes | QUOTE_READS_PER_IP_PER_10MIN | 28800 | GET /tokens/{address}/quote. A second, smaller budget covers quotes that miss the cache and reach the chain, and answers 429 {"error":"too many new quotes"} |
| Expensive reads | READS_PER_IP_PER_10MIN | 3000 | /pve/value, /status, /graduated/feed, /wallet/{address}/trades-today, /wallet/{address}/positions, /fees/history/{wallet} |
| Creators | CREATORS_TOP_READS_PER_IP_PER_10MIN | 3000 | GET /creators/top |
Every other public read is unlimited, /health and /health/live included. A refusal is
429 { "error": "rate limited" }.
/tokens, /pve/value and /creators/top add Retry-After in seconds plus Cache-Control: no-store on the refusal. The other throttled routes answer 429 with no Retry-After yet, so back
off on your own clock there rather than assuming the header. Because the buckets refill continuously,
Retry-After is the wait for one token and is usually about a second, not the rest of the window.
The stream is capped separately: 2000 connections globally and 40 per IP by default, both
env-configurable. Over the per-IP cap you get 429, over the global cap 503, each with
Retry-After.
Pagination, and caps where there is none
Two lists page with a cursor: the board and the graduated feed.
GET /tokens the whole board in one body: up to 500 coins, 100 pending
GET /tokens?limit=50 one page. limit is 1 to 100, default 50 once you page
GET /tokens?limit=50&cursor=... the next page, from the previous body's nextCursor
GET /tokens?q=cat a case-insensitive search, at most 64 characters, pages the same wayEvery board body carries nextCursor: a string when the page was full, null when you have reached
the end. Pages run newest coin first. The graduated feed works the same way and returns
{ coins, nextCursor }, with a default limit of 100.
The cursor is opaque and signed. Do not build one, edit one, or carry one from one search to
another: a cursor that fails its signature, or that was issued for a different q, answers
400 {"error":"bad cursor"}. A q longer than 64 characters answers 400 {"error":"bad q"}. The two
lists treat limit differently: on /tokens anything outside 1 to 100 answers
400 {"error":"bad limit"}, while on /graduated/feed only a non-number or a value below 1 is
refused, and a value above 200 is quietly clamped to 200. A cursor
can also stop verifying after a backend restart, so treat bad cursor as "start the scan again",
not as a fatal error.
Sending no parameters at all keeps the original behaviour, one body with the whole board, and that request is the one served from the 5-second cache. Prefer it for a poll and the paged form for a backfill.
Everything else has a server-side cap and no paging: 50 recent trades on a coin, 10 holders, 20
trades and 6 creations on /trades/recent, 25 creators, 200 fee claims.
/wallet/{address}/positions is the one uncapped list, dust-filtered.
If you need more than a cap gives you, index the events yourself. See scanners.
Response shapes are open, not closed
GET /tokens, GET /tokens/{address} and GET /intents/{address} select whole rows, so a schema
migration adds fields to the response automatically. GET /config and GET /version have grown
the same way: /config now also serves addresses.creatorFeeRegistry,
addresses.creatorFeeVault, publicOpen and launchOpensAt, and /version also serves dirty
and branch. Treat the documented field lists as at least
these fields and ignore what you do not recognise. Do not write a strict decoder that rejects
unknown keys.
Nullability is not uniform
ethUsd is nullable on /stats and /pve/value (a null means no real price was available, never a
fabricated one) and non-nullable on /tokens, /tokens/{address} and /creators/top (those fall
back to a configured rate). This is intentional. Do not assume one rule across the service.
metadata is null rather than wrong when the stored blob's hash disagrees with the on-chain
commitment. curve is null when the chain read failed past its stale window, with curveLive: false marking a stale-but-served state. A throttled RPC renders as curveLive: false, never as a
404.
Submitting a launch intent
POST /intents is open, gated by an EIP-712 signature over the payload and an IP rate limit of 5 per
10 minutes by default. The backend never holds launch authority: the chain re-verifies the same signature at
materialization, so a compromised backend cannot mint a coin.
LaunchIntent(address creator, address submitter, string name, string symbol,
bytes32 metadataHash, uint256 salt, uint256 nonce, uint256 deadline,
address feeRecipient)Domain: { name: "MotoswapLaunchpad", version: "1", chainId, verifyingContract: <curve> }, all four
from the served /config plus the chain you are on. Field order is load-bearing.
metadataHash is keccak256 of the exact serialized metadata JSON. nonce must equal the on-chain
launchNonce(creator) or you get a 409 naming the value to use. deadline is at most 7 days out.
submitter of 0x0 means anyone may submit the first buy; set it to yourself if you are taking a
dev buy, or the signature is a bearer token in the mempool.
Responses: 201 { predictedAddress, metadataHash }, 409 on a nonce or salt collision, 429 on
either throttle, 503 when the chain cannot be reached or moto.fun is not live on this chain.
Where to go next
- Full API inventory for exact request and response shapes.
- Realtime for the stream and the candle rules.
- AI agents for the read-only agent loop.