moto.fun API - Full Inventory
Every moto.fun REST endpoint across both services - path, params, response shape, caps, caching, and rate limits.
moto.fun API inventory
Complete Endpoint Reference for External Integrators
Two services carry moto.fun data. They are not the same backend and they do not share conventions.
| Motoswap API | The moto.fun app backend | |
|---|---|---|
| Base | https://api.motoswap.org | <motofunUrl>/backend-fun/<chain-slug> |
| Framework | Hono, OpenAPI-documented | Hono on Bun |
In /openapi.json | Yes | No |
| ETags | On cached GETs (not /config, /health/live, /version or writes) | Yes, on JSON 200 responses |
| Pagination | Cursor | None. Hardcoded caps |
| Auth | None on reads | None on reads |
| Realtime | No | SSE |
motofunUrl comes from GET https://api.motoswap.org/config. See
addresses.
PART 1 - MOTOSWAP API (api.motoswap.org)
Conventions
Wire types are the standard Motoswap ones: addresses lowercase strings, wei and uint256 as decimal
strings, USD as decimal-dollar strings ("1234.56"), seconds as numbers. Error body is
{ "error": "<string>" }. Non-2xx responses are never cached.
Cache tiers on this group: BLOCK is public, max-age=3, s-maxage=3, stale-while-revalidate=9.
STATIC is max-age=300, s-maxage=300, stale-while-revalidate=900.
ETag versioning: every /motofun/* route versions on the freshest block across the three
moto.fun raw tables, not on the chain head. A curve with no activity keeps returning 304 through
every DEX block. Send If-None-Match and poll freely.
Rate limit: /motofun/* is per-IP burst 600, 150 per second. /launchpad/* is burst 120, 30 per
second, tighter because the logo route serves bytes.
The 404 that is not a 404: every /motofun/* route answers
404 { "error": "moto.fun is not enabled on this deploy" } when the configured curve address is
zero. That is a deployment state, not a missing resource. Check /config.motofunEnabled first.
USD is frozen at write time. Every USD field except tvlUsd is a snapshot taken when the row was
written and is never re-priced. tvlUsd is a live gauge.
"Now" is the indexer head block timestamp, not wall clock, in every 24h window on this group.
GET /motofun/stats
Venue-wide counters. No parameters. Tier BLOCK.
{
"coins": 0,
"bonding": 0,
"frozen": 0,
"graduated": 0,
"tvlUsd": "0",
"volume24hUsd": "0",
"volumeAllUsd": "0",
"fees24hUsd": "0",
"feesAllUsd": "0",
"protocolFees24hUsd": "0",
"creatorFees24hUsd": "0",
"trades24h": 0,
"launches24h": 0
}coins,bonding,frozen,graduated,trades24h,launches24h: numbers- Every
*Usdfield: decimal-dollar string tvlUsdis the sum of real ETH in non-graduated curves at the live WETH price. Graduated coins' liquidity lives in pools and is counted by the DEX side, not herebonding = max(0, coins - graduated - frozen)
Statuses: 200, 404 (moto.fun off).
GET /motofun/coins
Newest-first coin list. Tier BLOCK, query allow-list ["cursor"].
| Param | In | Type | Required | Default |
|---|---|---|---|---|
cursor | query | string | no | none |
Page size is 25. The cursor is an opaque signed keyset cursor over (timestamp, token_address).
{
"items": [
{
"address": "0x…",
"name": "string",
"symbol": "string",
"creator": "0x…",
"timestamp": 0,
"status": "bonding",
"pairWeth": "0x…",
"pairMoto": "0x…",
"volume24hUsd": "0",
"volumeAllUsd": "0",
"trades": 0,
"lastPriceX18": "0"
}
],
"nextCursor": null,
"hasMore": false
}status:"bonding" | "frozen" | "graduated", folded on read from the latest surviving lifecycle row. A coin with no lifecycle row is"bonding"pairWeth,pairMoto: address ornull. Non-null only after graduationlastPriceX18: decimal string of the latest trade'sprice_x18, ornullif the coin never tradedtrades: number
A malformed cursor returns an empty 200, not a 403
There is no cursor error hook on this route. An undecodable cursor produces an empty page with
status 200. Check hasMore rather than treating an empty items as the end of the list.
Statuses: 200, 404 (moto.fun off).
GET /motofun/coins/{address}
Point lookup: is this a curve coin, and has it graduated?
| Param | In | Type | Required |
|---|---|---|---|
address | path | address | yes |
{
"address": "0x…",
"name": "string",
"symbol": "string",
"status": "bonding",
"pairWeth": "0x…",
"pairMoto": "0x…"
}No volume, price, or creator fields; that is the list shape only.
Statuses: 200; 404 { "error": "moto.fun is not enabled on this deploy" };
404 { "error": "this token never launched on the moto.fun curve" }. Tier BLOCK. The version is
venue-wide but the ETag folds the request path, so the tag is unique per address.
GET /motofun/revenue/series
Per-bucket venue revenue, Collector leg and creator leg separately.
| Param | In | Type | Required | Default | Values |
|---|---|---|---|---|---|
range | query | enum | no | "30d" | 7d, 30d, 90d, 1y |
bucket | query | enum | no | "day" | day, epoch |
{
"bucket": "day",
"buckets": [
{ "t": 0, "total": "0", "protocol": "0", "creator": "0" }
]
}t: bucket start, unix secondstotal,protocol,creator: decimal-dollar strings- The series is dense and zero-filled from the range start to the head bucket inclusive
protocolandcreatorare measured from theTradeevent's exact fee wei, never derived from a bps constant
Tier BLOCK, query allow-list ["range", "bucket"]. Statuses: 200, 404.
moto.fun on shared Motoswap routes
GET /config (tier STATIC) carries:
| Key | Type | Notes |
|---|---|---|
addresses.launchpadCurve | address | Zero when moto.fun display is off |
addresses.launchpadLens | address | Gated independently of the curve |
motofunEnabled | boolean | Exactly launchpadCurve != 0x0 |
motofunUrl | string | The moto.fun app origin. "" means fall back |
launchPhase | enum | "prelaunch" | "vamp" | "dex" |
GET /activity accepts venue: "motoswap" | "motofun" | "all", default "all". moto.fun rows
carry kind of motofun_launch, motofun_trade, or motofun_graduation. With moto.fun off, the
venue is forced to "motoswap" regardless of the request, because rows may exist in the database on
a display-off deploy.
GET /analytics/overview carries an optional top-level key, absent entirely when moto.fun is
off:
{ "motofun": { "volumeUsd": "0", "feesUsd": "0" } }moto.fun is an additive component here. The DEX revenue series (/analytics/revenue/series,
/stats/series) do not include curve volume; use /motofun/revenue/series for that.
GET /creator/{address} tags every coin with the surface that registered it:
{
"address": "0x…",
"tokens": [
{
"token": "0x…",
"disabled": false,
"venue": "motofun",
"lastClaimAt": 0,
"usdEarned": "0",
"quotes": [
{ "quoteAsset": "0x…", "earned": "0", "usdEarned": "0",
"swaps": 0, "claimed": "0", "unclaimed": "0" }
]
}
],
"series": [ { "day": 0, "usdEarned": "0", "swaps": 0 } ],
"totalUsdEarned": "0"
}venue is "motofun" | "deployer". deployer marks coins from the retired launch contract; new coins are always motofun. Both surfaces deposit into the same CreatorFeeVault, so
this is a label for display, never a branch in the claim path. A coin is "motofun" if and only if
it has a curve row on this chain. An unknown address returns 200 with empty arrays, never 404.
GET /creator/token/{token} does not carry venue.
GET /launchpad/token/{address}/logo serves a coin's logo as bytes.
200: image bytes,Content-Type: image/pngorimage/webp, tierSTATIC,ETag,X-Content-Type-Options: nosniff.304on a matchingIf-None-Match400 { "error": "invalid token address" },404 { "error": "no logo" }, bothprivate, no-store- On a local miss the API proxies once from the moto.fun backend, persists the result, and serves it locally from then on. Accepted payloads are PNG or WebP, non-empty, at most 256 KB and 512x512. A bounded negative cache stops address cycling turning this into an unmetered amplifier
- The
POSTon the same path is a separate signed-upload route that a moto.fun coin cannot use, which is why the proxy exists
Both logo routes are excluded from /openapi.json by design: one serves bytes, the other takes a
multipart body. Every /motofun/* route is in the spec, on a flag-off deploy as well, because the
flag decides whether a route answers, not whether it exists.
PART 2 - The moto.fun app backend
Base <motofunUrl>/backend-fun/<chain-slug>. Every path below is relative to that.
Conventions
- No auth on any read.
POST /intentsis gated by an EIP-712 signature, not by a key. The/admin/*group is operator-gated and answers401without a credential - ETags on JSON
200responses, weak (W/"<hex>"), over the serialized body.If-None-Matchgets a304with no body, carrying the sameETagandCache-Control. NoLast-Modified. No ETag on a redirect, on the PNG routes, or on any non-200 - Pagination on two lists only.
GET /tokensandGET /graduated/feedpage with a signed, opaque cursor and alimit. A forged, edited or stale cursor answers400 { "error": "bad cursor" }and an out-of-range limit400 { "error": "bad limit" }. Every other list has a fixed cap and no paging - Error body is
{ "error": "<string>" }, exceptGET /share/{address}, which answerstext/plainon error - Status codes in use: 200, 201, 302, 304, 400, 401, 403, 404, 409, 413, 429, 500, 503.
304answers a matchingIf-None-Match.302is the image route's redirect to the CDN.403is a banned party or a taken-down coin, both onPOST /intents - Body limit 256 KB; over it returns
413 { "error": "request body too large" } - Unhandled errors become
500 { "error": "internal error" }; the stack is never sent to the client
Rate limits. Per-IP token buckets, each over a 10-minute window with continuous refill. Each cap is env-configurable; the values below are the shipping defaults, not a contract.
| Limiter | Env var | Default | Applies to |
|---|---|---|---|
| Intent writes | INTENTS_PER_IP_PER_10MIN | 5 | POST /intents only |
| Board | BOARD_READS_PER_IP_PER_10MIN | 6000 | GET /tokens |
| Expensive reads | READS_PER_IP_PER_10MIN | 3000 | GET /pve/value, GET /status, GET /graduated/feed, GET /wallet/{address}/trades-today, GET /fees/history/{wallet}, GET /wallet/{address}/positions |
| Creators | CREATORS_TOP_READS_PER_IP_PER_10MIN | 3000 | GET /creators/top |
| Coin detail | TOKEN_DETAIL_READS_PER_IP_PER_10MIN | 30000 | GET /tokens/{address}, and one token per GET /tokens/{address}/snapshot |
| Quotes | QUOTE_READS_PER_IP_PER_10MIN | 28800 | GET /tokens/{address}/quote. A second, smaller budget covers quotes that miss the cache, refused with 429 { "error": "too many new quotes" } |
Every other public read is unlimited, /health included. Read-limit body is
{ "error": "rate limited" }. SSE is capped
separately at 2000 global (503 over it) and 40 per IP (429 over it) by default, both
env-configurable, and both refusals carry Retry-After.
GET /tokens, GET /pve/value and GET /creators/top answer a refusal with Retry-After in
seconds and Cache-Control: no-store. The remaining throttled routes send a bare 429 with no
Retry-After, so do not depend on the header being present. Under a continuous-refill bucket
Retry-After is the refill time for one token, about a second at these caps, and it is floored at 1
so it is never 0.
Cache headers are set per route from a table covering every route, and the coverage is enforced by a test that fails the build when a route has no entry.
| Tier | Wire value | Routes |
|---|---|---|
no-store | no-store | /health, /health/live, /version, /intents/precheck, POST /intents, /stream, every /admin/*, /wallet/{address}/fee-reroutes, /wallet/{address}/trades-today, /wallet/{address}/positions, /fees/history/{wallet} |
| Live revalidate | no-cache | /config, /intents/{address}, /tokens, /tokens/{address}, /tokens/{address}/candles, /tokens/{address}/holders, /trades/recent, /stats |
| Short window | public, max-age=10, stale-while-revalidate=20 | /creators/top |
| Short window | public, max-age=30, stale-while-revalidate=30 | /pve/value |
| Unfurl | public, max-age=300, stale-while-revalidate=600 | /share/{address} |
| Deprecated | public, max-age=300 | /tickers/{symbol}/available |
| Handler-owned | public, max-age=86400 (CDN redirect), public, max-age=300 (inline) | /tokens/{address}/image |
| Handler-owned | public, max-age=30 | /tokens/{address}/card.png |
no-cache stores the body and revalidates before every reuse; it is not no-store. The wallet money
routes are no-store rather than no-cache deliberately, so a claimable balance or a day's trades
cannot be served out of a shared cache to the wrong screen.
Two rules decide what a non-2xx gets, in this order. A no-store route stays no-store whatever it
answers, because that tier is applied before the status is looked at and nothing softens it. On every
other route, a handler that set its own Cache-Control keeps it; only when the handler set none does
an error get downgraded to no-cache, so a 404 or a 429 cannot inherit that route's positive
max-age.
Server-side caches: /tokens 5s, /stats 15s, /creators/top 20s, per-coin curve state 3s live
with a 60s stale-serve, PvE value 45s, share card 30s, the ETH/USD rate 60s, a creator's launch nonce
30s.
Response caps:
| Route | Cap |
|---|---|
/tokens coins / pending | 500 / 100 |
/tokens/{address} recentTrades | 50 |
/tokens/{address}/candles | 2000 buckets, plus one seed row |
/tokens/{address}/holders | 10 |
/trades/recent trades / creations | 20 / 6 (last 24h) |
/creators/top | 25 |
/wallet/{address}/trades-today | 50 |
/fees/history/{wallet} | 200 |
/wallet/{address}/positions | uncapped, dust-filtered |
Response shapes are open, not closed
/tokens, /tokens/{address} and /intents/{address} select whole database rows, so a schema
migration adds fields automatically. Treat every field list below as "at least these fields". Do not
write a decoder that rejects unknown keys.
GET /config
Static per deploy. No database read, no RPC. No parameters. Always 200.
{
"chainId": 1,
"addresses": {
"curve": "0x…",
"lens": "0x…",
"tokenImplementation": "0x…",
"pveVault": "0x…",
"weth": "0x…",
"moto": "0x…",
"creatorFeeRegistry": "0x…",
"creatorFeeVault": "0x…"
},
"eip712": { "name": "MotoswapLaunchpad", "version": "1" },
"motofunEnabled": true,
"publicOpen": true,
"launchOpensAt": null
}addresses.curve is the curve's proxy. creatorFeeRegistry and creatorFeeVault are read off the
curve after boot and serve null until that read lands: null means "not known yet", not the zero
address. publicOpen is an operator switch that gates the app's launch-pending screen and nothing
else; every route answers normally while it is false. launchOpensAt is an ISO 8601 UTC string or
null, and null means no opening time was given to this deploy.
addresses.pveVault is the zero address when PvE is not live on this chain, a declared value,
never an omitted key. motofunEnabled: false means the service is up but no curve is deployed
here.
GET /health
Operational. Unauthenticated by design. Do not point an integration at it as a data source.
{
"ok": true,
"chainId": 1,
"curve": "0x…",
"indexer": {
"lastIndexedBlock": "8123456",
"headBlock": "8123459",
"lagBlocks": 3,
"lastSuccessfulTickAgoMs": 1200,
"reorgCount": 0,
"lastReorgAgoMs": null,
"wedged": null
},
"frozenCoins": {
"count": 0, "hiddenCount": 0, "oldestSeconds": null,
"stuck": false, "maxSeconds": 300, "tokens": ["0x…"]
},
"keeper": {
"enabled": true, "address": "0x…", "balanceWei": "12000000000000000",
"lastPassAt": 1756000000000, "lastPassAgeMs": 4000,
"lastError": null, "stalled": false, "missing": false
},
"pveRecordFailures": { "count": 0, "tokens": [] },
"indexerErrors": {
"count": 0, "recentAgoMs": null,
"recent": [{ "txHash": "0x…", "logIndex": 3, "eventName": "Trade", "blockNumber": 8123400 }],
"maxAgeSeconds": 3600
}
}lastIndexedBlock and headBlock are decimal strings; lagBlocks is a number.
On a chain with no curve deployed, each section is replaced by a disabled string and the response
carries "mode": "pre-curve", still with 200.
Alert on ok, not on the status code
The HTTP code is 200 or 503 and covers only wedge, tick age and lag. The ok field is stricter:
it also requires no stuck graduations, a live keeper, and no recent indexer errors. 200 with
"ok": false is a real, reachable state.
GET /health/live
The liveness probe, and the path the platform healthcheck points at. One trivial check, no database
fold, no RPC, exempt from load shedding. No parameters. Always 200. Tier no-store, because a
liveness answer served from a cache reports a dead process as alive to the one reader that can
restart it.
{ "ok": true, "commit": "a1b2c3d" }Poll this rather than /health if all you need is up-or-down. /health is a deep status fold over
the database and is far more expensive to answer, but it is not rate-limited: an uptime probe of it
is never charged against any bucket.
GET /version
Build provenance: which build is serving right now. No parameters. Always 200. Tier no-store, for
the same reason as above inverted, a cached copy would report the previous commit after a deploy and
confirm a roll that did not take.
{ "commit": "a1b2c3d", "builtAt": "2026-01-01T00:00:00.000Z", "dirty": false, "branch": "master", "chainId": 1 }Use it to confirm a deploy actually landed, and to pin which build a bug report came from.
GET /tokens
The board. With no parameters it answers the whole board in one body, up to 500 coins and 100 pending, cached 5s, and that is the form to poll.
| Parameter | Type | Default | Notes |
|---|---|---|---|
limit | integer | 50 once limit or cursor is sent | 1 to 100, else 400 { "error": "bad limit" } |
cursor | string | none | The previous body's nextCursor, verbatim. Signed and bound to the q it was issued for, else 400 { "error": "bad cursor" }. Treat that as "restart the scan": a cursor can stop verifying across a backend restart |
q | string | none | Case-insensitive search, trimmed, at most 64 characters, else 400 { "error": "bad q" } |
Every body carries nextCursor: a string when the page came back full, null at the end. Pages run
newest coin first. 429 with Retry-After when the board bucket is empty.
{
"tokens": [
{
"address": "0x…", "creator": "0x…", "name": "…", "symbol": "…",
"metadata_hash": "0x…", "status": 1,
"pair_weth": null, "pair_moto": null,
"created_block": 123, "created_at": 1755990000,
"hidden": 0, "pve": 0, "frozen_at": null, "announced": 0,
"pve_credited_at": null, "fee_recipient": null,
"frozen_block": null, "graduated_block": null, "pve_credited_block": null,
"pair_moto_block": null, "unfrozen_block": null, "grad_mcap_usd": null,
"prev_frozen_at": null, "prev_frozen_block": null, "prev_unfrozen_block": null,
"trades24h": 4,
"last_price_x18": "1350000000",
"last_trade_ts": 1755999000,
"holder_count": 12,
"has_image": 1,
"volume24h": "410000000000000000"
}
],
"pending": [
{
"address": "0x…", "creator": "0x…", "submitter": "0x…",
"fee_recipient": "0x…", "name": "…", "symbol": "…",
"metadata_hash": "0x…", "nonce": "0", "deadline": 1756000000,
"has_image": 0
}
],
"ethUsd": 3421.55
}status:0None,1Trading,2Frozen,3Graduatedvolume24h: exact decimal wei string.last_price_x18: decimal stringhas_imageis advisory. It is a cheap pattern probe that skips hash verification, so an occasional 404 from the image route is expectedholder_countis the true net-positive holder count, netted of PvE fillspending[]is filtered against a live on-chain nonce so dead intents drop out, and includes intents up to one hour past their deadline so a card can show an expired end stateethUsdhere is never null; it falls back to a configured rate- Hidden coins are excluded throughout
GET /tokens/{address}
One coin. Fires a live curve read on every request, cached 3s.
Pending branch (an intent exists, no coin yet):
{ "pending": true, "intent": { "…every intents column…", "metadata_json": "{…}" }, "ethUsd": 3421.55 }Materialized branch: every column from /tokens above, plus:
{
"metadata": { "description": "…", "image": "data:image/png;base64,…" },
"curve": {
"realEth": "1200000000000000000",
"tokenReserve": "800000000000000000000000000",
"status": 1,
"buysPaused": false,
"paramsId": 0,
"priceX18": "1350000000",
"bondingProgress": "420000000000000000",
"frozenAtOnChain": null
},
"curveLive": true,
"curveReadAt": 1756000000123,
"recentTrades": [
{
"tx_hash": "0x…", "log_index": 3, "token": "0x…", "trader": "0x…",
"is_buy": 1, "eth_amount": "100000000000000000",
"token_amount": "1000000000000000000000",
"price_x18": "1350000000", "protocol_fee": "1000000000000000",
"creator_fee": "500000000000000", "block_number": 8123400,
"timestamp": 1755999000, "eth_amount_gwei": 100000000,
"creator_fee_gwei": 500000, "token_amount_milli": 1000000,
"fed": 0
}
],
"stats": {
"trades": 42,
"volume_wei": "4100000000000000000",
"creator_fees_wei": "20500000000000000",
"volume24h_wei": "410000000000000000"
},
"pveTokensWei": "0", "pveEthWei": "0",
"pveLaunchTokensWei": "0", "pveLaunchEthWei": "0",
"pveFedTokensWei": "0", "pveFedEthWei": "0",
"devHoldsWei": "0",
"firstBuyer": "0x…",
"frozenAt": null,
"sellsOpenAt": null,
"pveRecordFailed": null,
"creatorFeeRoutedWei": null,
"ethUsd": 3421.55
}metadatais a parsed object ornull. Null means the stored blob's hash disagrees with the on-chain commitment. It is never served unverifiedcurve.priceX18is served for every coin, graduated ones included, with no status gate:statussits beside it. Forstatus3it is the graduation price, frozen, equal to the coin's price when its reserve reached the floor. The live price of a graduated coin is its pair's.curve.paramsIdis the launch parameter set the coin was stamped withcurveisnullwhen the chain read failed and no recent good state exists.curveLive: falsemarks both that case and a stale-but-served state. A throttled RPC renders ascurveLive: false, never as a404recentTrades[]selects whole rows.fedis a 0/1 flag marking a buy whose whole fill escrowed to the PvE vault and which is not the coin's first buyfrozenAtandsellsOpenAtare non-null only while the coin is actually frozen, and both come from the same chain read so they cannot disagree.sellsOpenAt = frozenAt + SELL_REOPEN_DELAYpveRecordFailedisnullor{ "tokensWei": "…", "at": 1755999000 }creatorFeeRoutedWeiisnull(never happened) or a decimal wei stringdevHoldsWeiis always present as"0"rather than omitted
Statuses: 200, 404 { "error": "not found" }.
GET /tokens/{address}/candles
| Param | In | Type | Default | Bounds |
|---|---|---|---|---|
address | path | address | - | lowercased, not regex-validated |
resolution | query | integer seconds | 300 | clamped to [60, 86400] |
from | query | unix seconds | now - 86400 | raised to the window floor, then aligned down |
Response is a bare array, up to 2001 rows:
[
{ "bucket": 1755993600, "low": 1300000000, "high": 1400000000,
"trades": 6, "volume": "410000000000000000",
"open": "1310000000", "close": "1395000000" }
]The exact rules, including that empty buckets produce no row and fill-forward is the client's job,
are on realtime and candles. In short: UTC epoch-aligned buckets, a
2000-slot window anchored to the coin's last trade rather than to wall clock, one seed row
prepended, low/high as numbers and open/close as optional strings, volume as an exact wei
string summing both sides, and [] with 200 for a coin that never traded.
GET /tokens/{address}/holders
{
"holders": [ { "trader": "0x…", "net_wei": "1000000000000000000000" } ],
"holderCount": 40,
"pveVaultWei": "500000000000000000000"
}holders is the top 10 by net position. holderCount is the true total, not the page size.
pveVaultWei is a decimal wei string or null when nothing was banked. Positions are netted of
same-transaction PvE fills, so the escrow does not read as a whale. Always 200.
GET /tokens/{address}/image
The creator's picture as real bytes, so og:image works.
200: raw image body. Content-Type is one of image/png, image/jpeg, image/gif,
image/webp; plus cache-control: public, max-age=300, x-content-type-options: nosniff, and
content-security-policy: default-src 'none'. SVG is deliberately refused, as is any non-data
URL. Missing or unverified metadata gives 404 { "error": "no image" }.
GET /tokens/{address}/card.png
A 1200x630 share card. address is regex-validated here, 400 { "error": "bad address" }.
200 is PNG bytes with cache-control: public, max-age=30. 404 when no coin exists. Images are
re-checked against the on-chain hash before embedding; anything that fails degrades to a
ticker-letters placeholder.
GET /share/{address}
An HTML unfurl page for crawlers, with a meta-refresh bounce to the app for humans.
| Param | In | Notes |
|---|---|---|
address | path | On failure: text/plain 400 bad address |
ref | query | Optional referral, forwarded only if it is a valid address |
200 is text/html with og:title, og:description, og:image, twitter:card, and a refresh to
the app at /coin/<address>?chain=<slug>. 404 is text/plain.
GET /status
The public health answer, for telling an outage from your own network. No parameters. /health is
the operator document; this is the short one.
{ "state": "ok", "checks": [ { "name": "…", "state": "ok", "detail": "…" } ], "notice": null }notice is null or an object an operator has set, never a bare string:
{ "notice": { "text": "…", "severity": "warning", "setAt": 1755990000 } }severity is "info", "warning" or "critical", and setAt is unix seconds. Render
notice.text; printing the object gives [object Object]. Treat state and each check's state as
opaque labels to display, and do not build logic on the checks names, which are not a stable list.
Spends the expensive-reads bucket.
GET /tokens/{address}/quote
A lens quote served by the backend, so a client does not need its own RPC. side is buy or
sell. amount is a decimal wei string: ETH in for a buy, coin in for a sell.
{ "side": "buy", "tokensOut": "…", "grossUsed": "…", "refund": "…", "quotedAmount": "…" }A sell answers ethOut in place of the three buy fields. 400 on a malformed address, side or
amount. 502 { "error": "quote unavailable" } when the chain read failed. 429 on either of its two
budgets, with Retry-After. It is a convenience over LaunchpadLens.quoteBuy and quoteSell, which
stay the canonical source and are what to use before sending a transaction.
GET /tokens/{address}/snapshot
The coin page in one body: { token, holders }, plus candles and quote when their parameters are
sent. token is exactly the GET /tokens/{address} body and holders exactly the
GET /tokens/{address}/holders body, built by the same code behind the same caches, so the field
lists for those routes apply unchanged. It spends one token on each bucket whose data it carries.
Use it when you would otherwise make those requests together; keep the separate routes when you
need one of them.
GET /graduated/feed
Graduated coins, newest graduation first. Built for listing sites.
| Parameter | Type | Default | Notes |
|---|---|---|---|
limit | integer | 100 | At least 1, clamped to 200. A non-number answers 400 { "error": "bad limit" } |
cursor | string | none | The previous body's nextCursor. Signed; a bad one answers 400 { "error": "bad cursor" } |
{
"coins": [
{
"chainId": 1, "token": "0x…", "name": "…", "symbol": "…", "creator": "0x…",
"pair": "0x…", "pairMoto": "0x…",
"graduatedAt": 1755990000, "graduatedBlock": 123,
"logoUrl": "…", "url": "…"
}
],
"nextCursor": null
}pair is the TOKEN/WETH pool and pairMoto the TOKEN/MOTO pool, the same two addresses the
Graduated event carries. Hidden coins are left out. 429 with Retry-After when rate limited.
GET /trades/recent
The live ticker strip. No parameters, no rate limit. Always 200.
{
"trades": [
{ "tx_hash": "0x…", "log_index": 3, "token": "0x…", "trader": "0x…",
"is_buy": 1, "eth_amount": "100000000000000000",
"timestamp": 1755999000, "symbol": "DOGE",
"block_number": 8123400, "fed": 0 }
],
"creations": [
{ "token": "0x…", "creator": "0x…", "symbol": "DOGE",
"timestamp": 1755990000, "block_number": 8123000 }
]
}20 newest trades by (block_number DESC, log_index DESC). 6 newest creations, only from the last
24 hours. Hidden coins excluded.
GET /stats
No parameters. Cached 15s. Always 200.
{
"coins": 128, "graduated": 9,
"volumeWei": "412000000000000000000",
"creatorFeesWei": "2060000000000000000",
"pveTokensWei": "0", "pveEthWei": "0",
"pveUnvaluedCount": 0,
"ethUsd": 3421.55
}The three *Wei fields are exact decimal strings. ethUsd is nullable here: a null means no real
price was available, never a fabricated one.
GET /creators/top
No parameters. Cached 20s. Always 200.
{
"rows": [
{ "creator": "0x…", "earned_wei": "1200000000000000000",
"trades": 84, "coins": 3,
"top": { "address": "0x…", "name": "Doge", "symbol": "DOGE", "status": 3 } }
],
"totalWei": "2060000000000000000",
"ethUsd": 3421.55
}Folded per creator, not per coin, capped at 25. top is that creator's highest-earning coin.
ethUsd here is not nullable.
GET /pve/value
What the PvE escrow is currently worth on this chain. Memoised 45s and coalesced across callers.
{
"chainId": 1, "ethWei": "3200000000000000000",
"curvePriced": 12, "poolPriced": 3, "unpriced": 1,
"asOf": 1756000000, "ethUsd": 3421.55
}A graduated coin with a WETH pair is priced off pool reserves; everything else off its last indexed
trade price. A coin that cannot be priced is counted in unpriced rather than silently zeroed.
ethUsd is nullable.
GET /wallet/{address}/positions
Read-rate-limited. address is regex-validated.
{
"positions": [
{ "token": "0x…", "symbol": "DOGE", "name": "Doge", "status": 1,
"last_price_x18": "1350000000",
"net_wei": "1000000000000000000000",
"eth_in_wei": "100000000000000000",
"bought_wei": "1200000000000000000000" }
]
}Uncapped, dust-filtered, ordered by position value descending. Cost basis is apportioned by the PvE net ratio. Hidden coins excluded.
GET /wallet/{address}/trades-today
Read-rate-limited. The wallet's trades since the last 00:00 UTC Points drop. Deliberately carries no point values and no per-trade deltas.
{
"count": 7,
"dayStart": 1755993600,
"trades": [
{ "txHash": "0x…", "token": "0x…", "name": "Doge", "symbol": "DOGE",
"isBuy": true, "fed": 0,
"ethAmount": "100000000000000000", "timestamp": 1755999000 }
]
}count is the true day total; trades is capped at 50. dayStart is computed from the server
clock, not chain time.
GET /wallet/{address}/fee-reroutes
Not rate-limited. Every coin this creator launched whose creator fee was rerouted to the Collector, in one request.
{ "reroutes": { "0xtoken…": "1500000000000000" } }A map of coin address to cumulative rerouted wei. {} is the normal case.
GET /fees/history/{wallet}
Read-rate-limited. Lifetime creator-fee claims. The recipient matched is whoever the vault
actually paid, which is not necessarily the launch signer.
{
"lifetime": { "0xquoteasset…": "4200000000000000000" },
"claims": [
{ "txHash": "0x…", "token": "0x…", "name": "Doge", "symbol": "DOGE",
"quoteAsset": "0x…", "amountWei": "1200000000000000000",
"timestamp": 1755999000 }
]
}lifetime is keyed by quote asset, summed in BigInt rather than in SQL to keep wei precision.
claims is capped at 200, newest first. Carries no live accrued balances: read those from the
vault on chain.
GET /intents/precheck
A read-only advisory answered before the wallet signature prompt. It peeks the rate-limit window without consuming a slot.
| Param | In | Required | Validation |
|---|---|---|---|
creator | query | yes | address, else 400 { "error": "bad creator address" } |
symbol | query | no | trimmed and lowercased, checked against the reserved list |
{ "throttled": false, "reserved": false, "slotsFree": 3 }slotsFree fails open to the full cap on an RPC failure. Statuses 200, 400.
POST /intents
Creates a gasless launch intent. Signature-gated and IP-rate-limited at 5 per 10 minutes. The chain re-verifies at materialization, so the backend never holds launch authority.
| Field | Type | Required | Default | Validation |
|---|---|---|---|---|
creator | address | yes | - | address regex |
submitter | address | no | zero | Zero means anyone may submit |
feeRecipient | address | no | zero | Refused if it is the curve or the PvE vault |
name | string | yes | - | 1 to 48 characters |
symbol | string | yes | - | ^[A-Za-z0-9]{1,16}$, and not reserved |
salt | integer string | yes | - | must parse as a BigInt |
nonce | integer string | yes | - | must equal the on-chain launchNonce, else 409 |
deadline | integer string | yes | - | a safe integer, in the future, at most 7 days out |
signature | hex string | yes | - | EIP-712 recovery must equal creator |
metadata | any JSON | no | {} | serialized at most 20,000 characters |
The EIP-712 type, field order load-bearing:
LaunchIntent(address creator, address submitter, string name, string symbol,
bytes32 metadataHash, uint256 salt, uint256 nonce, uint256 deadline,
address feeRecipient)Domain: { name, version, chainId, verifyingContract }, with name and version from the served
/config.eip712 and verifyingContract the served curve. metadataHash is keccak256 of the exact
serialized metadata JSON.
201:
{ "predictedAddress": "0x…", "metadataHash": "0x…" }Other statuses: 400 on any validation failure, including bad signature. 409 on a nonce mismatch
(the message names the value to use) or a salt that already launched a coin. 429 on the IP throttle
or the per-creator live-intent cap. 503 when moto.fun is not live on this chain, or when the chain
cannot be reached to read the nonce.
Re-posting the same (creator, salt) upserts, preserving the original created_at and any
moderation state.
GET /intents/{address}
A pending launch's signed payload, verbatim, because it is what a submitter must replay on chain.
{
"predicted_address": "0x…", "creator": "0x…", "submitter": "0x…",
"fee_recipient": "0x…", "name": "…", "symbol": "…", "symbol_lower": "…",
"metadata_hash": "0x…", "metadata_json": "{…}",
"salt": "123", "nonce": "0", "deadline": 1756000000,
"signature": "0x…", "created_at": 1755990000, "materialized": 0
}Every field except metadata_json travels verbatim. metadata_json is a string or null, nulled
when its hash does not match the on-chain commitment. Hidden intents 404.
GET /tickers/{symbol}/available - DEPRECATED
Ticker exclusivity was removed from the contract. This always answers { "available": true } for a
syntactically valid symbol, and { "available": false, "reason": "1-16 letters or numbers" }
otherwise. Always 200. Do not integrate against it.
GET /stream
Server-Sent Events. Documented in full on realtime. Summary: a
firehose with no subscription protocol, payloads of exactly { type, token }, event types created,
trade, frozen, unfrozen, graduated (plus two retired ones), a ping every 25 seconds, the SSE
connection caps described above (2000 global and 40 per IP by default), and no replay: no id:, no Last-Event-ID, no buffer.
Not for integration
- The
/admin/*group: moderation, an operator identity read, the audit log, metadata override read and write and delete, ban and unban, and the ban list. Nine routes. All operator-gated, allno-store,401without a credential. All nine share the same operator prologue, so all nine answer503when no operator token is configured, which means the whole group is off by default. Shapes are deliberately not documented here. Never document the credential, never integrate against any of it
Indexing model, both services
Both indexers are polling log indexers over the curve address.
Events consumed. The app backend indexes TokenCreated, Trade, FloorReached, Graduated,
PveFill, PveRecorded, PveRecordFailed, FeeRecipientSet, CreatorFeeRoutedToCollector, plus
Claimed from the CreatorFeeVault. The Motoswap API indexes TokenCreated, Trade,
FloorReached, PveFill, Graduated, and recognises and skips everything else on the curve.
Reorg discipline. The app backend scans to head - confirmations (default 3), detects reorgs by
comparing the stored canonical block hash rather than by height, rewinds max(4 x confirmations, 64)
blocks inside one transaction, and purges every block-scoped table above the fork point before
re-deriving. Over-rewinding is free because every insert is idempotent on (tx_hash, log_index).
Past a 200-block cumulative rewind it wedges deliberately and stops, reporting it on /health, until
a human clears it.
The Motoswap API keeps no derived accumulator for moto.fun: status folds on read and volume sums surviving rows, so a rollback is a range delete with nothing to rebuild.
Graduation market cap is snapshotted once per graduation and never re-priced. A graduation reorged away clears it.
Poison logs are caught per log, recorded, and skipped rather than wedging the cursor, which is why
/health alerts on the recency of those rows rather than only on their count.
moto.fun Contracts - Full Inventory
Complete API reference for external integrators - every moto.fun Solidity contract, function, event, error, constant, access rule and invariant, plus the canonical contracts the curve depends on.
moto.fun Addresses, Config and Events
Addresses and discovery, chain config, fee constants, and every moto.fun event signature with its indexed fields.