moto.fun

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 APIThe moto.fun app backend
Basehttps://api.motoswap.org<motofunUrl>/backend-fun/<chain-slug>
In /openapi.jsonYesNo
ETags / 304Yes, every routeYes, on JSON 200 responses
PaginationCursor, { items, nextCursor, hasMore }Signed cursors on the board and the graduated feed. Hardcoded caps everywhere else
RealtimeNoServer-Sent Events
CandlesNoYes
AuthNoneNone on reads
Use it forDiscovery, listings, venue stats, revenueCharts, 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

EndpointPurpose
GET /motofun/statsVenue 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:

Endpointmoto.fun content
GET /configaddresses.launchpadCurve, addresses.launchpadLens, motofunEnabled, motofunUrl
GET /activity?venue=motofunThe unified feed, filtered to motofun_launch, motofun_trade, motofun_graduation
GET /analytics/overviewAn 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}/logoA coin's logo as bytes, mirrored from moto.fun

Semantics that will bite you

  • A 404 usually means "moto.fun is off on this deploy", not "no data". Every /motofun/* route answers 404 {"error":"moto.fun is not enabled on this deploy"} when the curve address is zero. Check /config first and you will never see that one. The point lookup has a second 404 of its own: with the venue on, GET /motofun/coins/{address} answers 404 {"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 reading motofunEnabled first.
  • status is 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 a 403. Do not read an empty page as "end of list" without checking hasMore.
  • /motofun/* ETags version on the curve's own head block, so they stay 304 through DEX-only blocks. Poll them freely with If-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.

EndpointPurpose
GET /configCurve, lens, token implementation, PvE vault, WETH, MOTO, the EIP-712 domain
GET /healthIndexer lag, frozen coins, keeper state, PvE failures. Operational, not a data source
GET /health/liveLiveness only: { ok, commit }. Poll this rather than /health
GET /versionWhich build is serving right now: { commit, builtAt, chainId }
GET /statusThe 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}/snapshotThe 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}/holdersTop 10 net holders, the true holder count, and the PvE escrow balance
GET /tokens/{address}/imageThe creator's picture as bytes, re-verified against the on-chain hash
GET /tokens/{address}/card.pngA 1200x630 share card
GET /share/{address}The crawler unfurl page. HTML, and text/plain on error
GET /trades/recent20 newest trades and 6 newest launches from the last 24h
GET /statsCoin counts, volume, creator fees, PvE totals
GET /creators/topTop 25 creators by fees earned, folded per creator
GET /pve/valueWhat the PvE escrow is currently worth on this chain
GET /wallet/{address}/positionsNet positions with cost basis. Rate-limited
GET /wallet/{address}/trades-todayThe wallet's trades since 00:00 UTC today. Rate-limited
GET /wallet/{address}/fee-reroutesCoins 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 /intentsSubmit a signed LaunchIntent. See below
GET /intents/{address}A pending launch's signed payload, verbatim
GET /streamServer-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.

TierRoutes
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.

BucketEnv varDefaultApplies to
Intent writesINTENTS_PER_IP_PER_10MIN5POST /intents
BoardBOARD_READS_PER_IP_PER_10MIN6000GET /tokens
Coin detailTOKEN_DETAIL_READS_PER_IP_PER_10MIN30000GET /tokens/{address}, and one token per snapshot
QuotesQUOTE_READS_PER_IP_PER_10MIN28800GET /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 readsREADS_PER_IP_PER_10MIN3000/pve/value, /status, /graduated/feed, /wallet/{address}/trades-today, /wallet/{address}/positions, /fees/history/{wallet}
CreatorsCREATORS_TOP_READS_PER_IP_PER_10MIN3000GET /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 way

Every 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

On this page