Motoswap REST API - Full Inventory
Complete REST API reference for integrators - every endpoint, params, response shape, cache tier.
Base URL: https://api.motoswap.org
CORS & Deployment
- CORS: the policy is set for the Motoswap app origin. From another origin a plain
GETwith no custom headers works in a browser, and every preflighted request is blocked (afetchthat setsIf-None-Match, any JSONPOST). Run conditional polling and writes server-side, or rely on the browser's own HTTP cache, which revalidates without a preflight. - Rate limits and load shedding: routes are rate-limited per IP (
429+Retry-After), and heavy routes shed with503+Retry-Afterunder load. Honor the header and back off. - Chain ID: this host serves Ethereum mainnet, chain id
1.GET /configandGET /versionboth echo it.
Which routes carry data
Deployment status is per contract: see the status table on the contract addresses page. GET /config is the machine-readable version, and a key holds the zero address while its contract is unset for the current phase.
- Token, price, pool, swap-list, farm, stake and MOTO supply routes answer with data, from the Motoswap and Uniswap V2 pairs the indexer tracks.
- Candle arrays,
/analytics/*,/stats/series,/rakeback/*,/creator/*,/pve/*and LP fees are driven by the protocol fee, so a wallet, token or window with no fee-paying trades answers empty rather than with an error. launchPhaseonGET /configis the machine-checkable switch:dexwith the DEX contracts deployed and open,vampbefore the DEX opened. It does not track launch farming (ended), which kept paying for part of the day after the switch todex; the farm's own end isemissionEnd()onVampChef.- Responses can carry fields that are not listed here. Ignore fields you do not recognise; do not build a parser that rejects them.
- An empty answer has the same shape as a populated one. The examples below show populated shapes taken from the response contract.
Wire types and encoding
All field types are JSON-safe. Here's the domain type mapping:
| Domain Type | Wire Format | Notes |
|---|---|---|
Usd | usdWire (string) | decimal-dollar string, e.g. "2500.50" = $2,500.50; parse as a decimal, never as an integer |
Wei | weiWire (string) | Token base units (scale by the token's decimals); bigint serialized as string |
Address | lowercase 0x… (string) | Lowercased 0x-prefixed Ethereum address, branded type |
TxHash | 0x… (string) | 0x-prefixed 32-byte transaction hash, branded |
Seconds | number (Unix) | Integer Unix seconds |
Percent | number | Percentage float (e.g., 5.2 = 5.2%); null when uncomputable |
Apr | number | Integer basis points (e.g., 1250 = 12.50% APR); null when uncomputable |
Parse Wei strings with BigInt(value), never Number(value). USD examples on this page are decimal dollars with up to six decimal places, as served ("878879.607649").
Cache and ETag semantics
Cached GET endpoints emit Cache-Control: public, max-age=N, s-maxage=N, stale-while-revalidate=M per tier:
| Tier | Max-Age | SWR | Use Case |
|---|---|---|---|
HEARTBEAT | 1s | 9s | Chain head (block number) |
BLOCK | 3s | 9s | Indexer-fresh data (TVL, volume, pricing, per-user rakeback) |
CRON | 30s | 90s | Cron-derived data (/analytics/breakdown, leaderboard, weeks, leagues, candidates, /explore/creators) |
STATIC | 300s | 900s | Config, /points/rules, /rakeback/stats, immutable history |
(CRON_FAST (15s/45s) is mounted on one route: GET /profiles.)
ETag Revalidation: Cached endpoints emit a weak ETag (W/"...") and support If-None-Match; send the tag back verbatim and a match answers 304 (not modified). Integrators should cache ETags per URL. GET /config carries Cache-Control but no ETag. /health/live, /version and every write are private, no-store.
Cursor pagination model
List endpoints use keyset (cursor) pagination: they return nextCursor (opaque HMAC-signed token). The cursor is scope-bound to the endpoint path and query filters:
- A cursor is not replayable across queries with different
?filter=valueparams. - Tampered/replayed cursors → 403 (not 400), body
{"error":"invalid cursor"}. - Always pass the same filters on every page to continue.
Cursor routes: /activity, /dex/pools, /explore/tokens, /explore/farms, /explore/creators.
(The bounded sets /farms and /swap/candidates are served whole, unpaginated. /tokens/{address}/swaps, /tokens/{address}/pools and /dex/pools/{pair}/swaps take ?page= instead of a cursor.)
Error envelope
All errors are JSON:
{ "error": "message string" }A path or query value that fails schema validation (a malformed address, for example) can answer with the validator's shape instead: { "success": false, "error": { "name": "ZodError", "message": "..." } }. Branch on the status code, not the body.
Common status codes:
400- bad request (validation error, invalid filter, etc.)403- tampered/invalid cursor, signature mismatch, unauthorized404- not found (unknown pair, unknown route)429- per-IP rate limit, withRetry-After503- service temporarily degraded (indexer lag, upstream)
Route by route
health.ts
GET /health/live
Summary: Liveness probe. One SELECT 1, exempt from load shedding. This is the route to poll.
Response (200):
{ "ok": true }Response (503): { "ok": false } when the database is unreachable.
Caching: private, no-store (no caching).
Notes: The deep health routes are operational monitoring, not integration surfaces. They are rate-limited and their shape changes without notice.
version.ts
GET /version
Summary: Build identity of the running API and the chain it serves.
Response (200):
{
"commit": "<40 hex characters>",
"hasProvenance": true,
"buildId": "<uuid>",
"service": "api",
"chainId": 1,
"bootedAt": 1789697310
}Response fields:
commit: string- source commit of the build, or"unknown"hasProvenance: boolean- whether the commit was stamped by the buildbuildId: string- deploy idchainId: number- the chain this deployment servesbootedAt: number- Unix seconds of process start
Caching: private, no-store.
config.ts
GET /config
Summary: Smart contract addresses and feature flags for the active chain. Critical for initializing the client.
Response (200):
{
"chainId": 1,
"addresses": {
"factory": "0x...",
"router": "0x...",
"permit2": "0x...",
"motoStaking": "0x...",
"motoToken": "0x...",
"masterChef": "0x...",
"tokenLauncher": "0x...",
"feeRouter": "0x...",
"collector": "0x...",
"quoteRegistry": "0x...",
"rakeback": "0x...",
"zap": "0x...",
"vaultCompounder": "0x...",
"usdc": "0x...",
"usdt": "0x...",
"weth": "0x...",
"creatorFeeRegistry": "0x...",
"creatorFeeVault": "0x...",
"motocatNft": "0x...",
"motocatStaking": "0x...",
"pveVault": "0x...",
"pveVaultV2": "0x...",
"buybackBurner": "0x...",
"buybackDistributor": "0x...",
"rewardVestingEscrow": "0x...",
"treasuryVesting": "0x...",
"launchpadCurve": "0x...",
"launchpadLens": "0x...",
"motoUniswapPair": "0x...",
"vampChef": "0x...",
"uniswapZap": "0x...",
"uniswapV2Router": "0x...",
"uniswapV2Factory": "0x...",
"vaults": ["0x...", "0x...", "0x...", "0x..."]
},
"splitSwapEnabled": false,
"motofunEnabled": true,
"bridgeMotoEnabled": false,
"motofunUrl": "https://...",
"pveComboEnabled": false,
"launchPhase": "dex",
"misconfigured": []
}Caching: Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=900 (5 minutes, STATIC tier). No ETag on this route.
Notes:
- The example gives the response shape, not values: every address is a placeholder and the flags are illustrative. Call the endpoint for current values. A key carries an address once its contract is deployed and reads
0x0000000000000000000000000000000000000000while it is unset for the phase. Some keys stay zero permanently (optional or retired contracts), so check every key you use and treat a zero address as unset, never as a target. The per-contract status table is on the contract addresses page. masterChefandvampChefheld the same contract whilelaunchPhasewasvamp. A farm is identified by(chef, pid); readchefoff each/farmsrow.permit2,zap,vaultCompounder,usdc,creatorFeeRegistry,creatorFeeVaultare optional (may be0x0…).splitSwapEnabledis an API configuration flag read by the Motoswap app:truemeans the app submits a split fill as oneswapExact*Splittransaction,falsemeans one swap per leg. It is not an on-chain switch. TheFeeRoutersplit entrypoints have no enable flag.launchPhaseisprelaunch | vamp | dex.misconfigured: string[]is the ops signal: it names the critical address keys left at0x0…for the currentlaunchPhase(empty when all are set). The endpoint always answers 200, including when the list is non-empty, because it also carrieslaunchPhaseand a refusal would leave the client guessing the phase.motofunEnabled,motofunUrl,bridgeMotoEnabledandpveComboEnabledare configuration flags carried alongside the addresses.
chain.ts
GET /chain/head
Summary: Current indexer tip (block number + timestamp). Integrators poll this to detect indexer staleness.
Response (200):
{
"chainId": 1,
"blockNumber": 26003732,
"blockTimestamp": 1789726607
}Response (200, no indexer state):
{
"chainId": 1,
"blockNumber": 0,
"blockTimestamp": 0
}Caching: Cache-Control: public, max-age=1, s-maxage=1, stale-while-revalidate=9 (HEARTBEAT - 1s).
Notes: Used to version per-user and per-address ETags. Rapid polling (e.g., every 500ms) is safe; cache will serve 304.
token.ts
GET /token/{address}/price
Summary: Current USD price of a token.
The price route is singular, /token/{address}/price. The chart route is plural, /tokens/{address}/chart. GET /tokens/{address}/price and GET /token/{address}/chart both answer 404.
Path params:
address- token address,0xplus 40 hex characters. Case does not matter: the lowercase, checksummed and uppercase-hex forms all answer 200 with the same token's price. Any other length, or a missing0x, answers 400.
Response (200):
{
"priceUsd": "0.003295",
"source": "onchain",
"lastUpdated": "YYYY-MM-DDTHH:mm:ss.sssZ"
}Response (200, no price):
{
"priceUsd": null,
"source": null,
"lastUpdated": null
}Response fields:
priceUsd: string | null- decimal-dollar string;nullif unpricedsource: string | null- pricing source label ("onchain"for pool-derived prices)lastUpdated: string | null- ISO 8601 timestamp with milliseconds, or null. It is the timestamp stored on the token's price row and it can trail the price itself, so useGET /chain/headto judge indexer freshness
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier - price moves on the write path per block).
Error responses: 400 (invalid address).
Notes: Prices are decimal-dollar strings; ethPrice (integer Wei per token) on /tokens/{address}/stats and /holdings/tokens carries full precision. An unknown token answers 200 with all three fields null, not 404. This is the per-token price read to use for MOTO: GET /token/0xbd965230588eaa536de6aa45e8ebbc01638535e0/price.
tokens.ts
GET /tokens
Summary: Token directory - all curated + launched tokens with symbol/name/decimals.
Response (200):
{
"tokens": [
{
"address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599",
"symbol": "WBTC",
"name": "Wrapped BTC",
"decimals": 8,
"verified": true,
"hasLogo": false
}
],
"generatedAt": "YYYY-MM-DDTHH:mm:ss.sssZ"
}Response fields:
tokens[].address: string- token addresstokens[].symbol: string- symboltokens[].name: string | null- name or nulltokens[].decimals: number | null- decimals or nulltokens[].hasLogo: boolean- whether a logo upload existstokens[].verified: boolean-true= an env-canonical token (WETH/USDC/USDT/MOTO) or a curated registry/seed row;false= permissionless onchain discovery
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier, indexer-head versioned).
Notes:
- Merges the curated token directory with launched tokens (launched tokens take lower priority on conflict).
- Launched tokens always have
decimals: 18. - Curated tokens may have RPC-resolved decimals.
- Empty response is never 404; returns
[]if no tokens.
GET /tokens/{address}/chart
Summary: Token price candlesticks + time-series volume/fees.
Path params:
address- token address
Query params:
range(1m|5m|10m|30m|24h|7d|30d, default24h) - chart frame
Response (200):
{
"price": "0.003295",
"change5mPct": 0.5,
"change30mPct": 1.2,
"change6hPct": -2.1,
"change24hPct": 5.0,
"volume5mUsd": "2769.116479",
"volume1hUsd": "26792.557773",
"volume24hUsd": "1305195.246871",
"fees24hUsd": "3915.584766",
"swapCount24h": 1955,
"bucketMinutes": 1,
"candles": [
{
"minute": 29828778,
"open": "0.0003459298143979567603005925",
"high": "0.0003459298143979567603005925",
"low": "0.0003454649992475997730919503",
"close": "0.0003454649992475997730919503",
"volumeUsd": "199.325593"
}
]
}Response fields:
price: string | null- current spot price (decimal-dollar string) or nullchange*Pct: number | null- percent change (e.g.,5.0= +5%), or nullvolume*Usd: string- decimal-dollar volume string;"0"if none (never null)fees24hUsd: string- 24h protocol fees (decimal-dollar string)swapCount24h: number- transaction countbucketMinutes: number- candle granularity (1, 30, or 120 min per range)candles[]: { minute, open, high, low, close, volumeUsd }- sorted oldest→newest.minuteis a number, the bucket start in unix MINUTES (multiply by 60 for seconds). OHLC values are decimal strings
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Error responses: 400 (invalid address/range).
Notes:
- Empty chart (untraded token) returns candles:
[], price/deltasnull, volumes"0". Candles come from Motoswap pairs, so a token with no Motoswap trading history answers this way. For a spot price useGET /token/{address}/priceorGET /holdings/tokens. - Change % is computed from the first non-zero bucket; null if all zeros.
- The short frames
1m/5m/10m/30mare named for their CANDLE interval, over windows of 6h/12h/24h/3d respectively. The long frames24h/7d/30dare named for their window, with 1/30/120-minute buckets. ReadbucketMinutesoff the response rather than inferring it fromrange.
GET /tokens/{address}/stats
Summary: TVL + 24h volume + integer ETH price for token-detail stat tiles.
Path params:
address- token address
Response (200):
{
"tvlUsd": "878879.607649",
"volume24hUsd": "1329720.547149",
"ethPrice": "1313311934801"
}Response fields:
tvlUsd: string- total liquidity value (decimal-dollar string);"0"if no poolsvolume24hUsd: string- 24h volume (decimal-dollar string)ethPrice: string | null- Wei per token (integer, full precision); null if unpriceable
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes:
ethPriceis the canonical full-precision price used to compute FDV/market cap on the FE.tvlUsdcoerces to"0"(never null) when unpriced.
GET /tokens/{address}/swaps
Summary: Paginated swap transactions for this token.
Path params:
address- token address
Query params:
page(int, 1-200, default 1) - page number
Response (200):
{
"items": [
{
"txHash": "0xabcd...",
"logIndex": 195,
"timestamp": 1789466771,
"side": "sell",
"tokenAmount": "133036509349955481108480",
"tokenDecimals": 18,
"counterToken": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"counterAmount": "180794401376581640",
"counterDecimals": 18,
"usdValue": "448.176348",
"wallet": "0x..."
}
],
"page": 1,
"pageSize": 50,
"hasMore": true
}Response fields:
side: "buy" | "sell"- direction from this token's perspectivetokenAmount, counterAmount: string- WeitokenDecimals: number- this token's decimalscounterToken: string | null,counterDecimals: number | null- the other side of the trade, or null when it cannot be resolvedusdValue: string | null- decimal-dollar USD value at swap time, or nullwallet: string- swapper addresspoolCount?: number,outerInput?: { token, amount }- optional, present only on rows that fold a multi-pool trade into one linepage, pageSize, hasMore- this list has nototal. Keep requestingpage + 1whilehasMoreis true
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Pagination: Page-numbered, 50 items/page, page 1 to 200.
GET /tokens/{address}/pools
Summary: Paginated pools containing this token.
Path params:
address- token address
Query params:
page(int, 1-200, default 1)
Response (200):
{
"items": [
{
"pairAddress": "0x...",
"token0": "0x...",
"token1": "0x...",
"reserve0": "133292018598175373111408005",
"reserve1": "175053998838797275747",
"lastUpdated": 1789726715,
"tvlUsd": "878879.607649",
"volume24hUsd": "1305195.246871",
"fees24hUsd": "3915.584766",
"feeAprBps": 16261,
"tvlDeltaPct": 5.2,
"volume24hDeltaPct": -3.1,
"fees24hDeltaPct": 2.0
}
],
"page": 1,
"totalPages": 1,
"total": 31
}Pagination: Page-numbered, 50 pools/page, with totalPages and total (unlike the swaps list above).
Notes: Pool deltas and APR are nullable when uncomputable. The three *DeltaPct fields are null until the pool has enough candle history to compare against.
GET /tokens/moto/supply
Summary: MOTO supply breakdown, read from chain, with the circulating figure and its definition.
Response (200):
{
"totalSupply": "10000000000000000000000000000",
"burned": "0",
"treasuryVestingUnreleased": "1900000000000000000000000000",
"treasuryLocked": "2099456366000000000000000256",
"escrowUnreleased": "10306557902037501334536150",
"chefUndistributed": "499307359307359307359314622",
"circulating": "5490929716790603191306148972",
"definition": "Total supply minus burned MOTO, minus ..."
}Response fields:
- Every amount is a Wei string (18 decimals). Parse with
BigInt circulating: string- total supply minus the five deductions above itdefinition: string- the plain-English definition ofcirculating, served with the number so a listing site can quote it
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier). The chain reads behind it are cached in process for about a minute.
token-logo.ts
GET /launchpad/token/{address}/logo
Summary: Fetch uploaded logo image for a launched token.
Path params:
address- launched token address
Response (200): Binary image data (PNG or WebP).
Response Headers:
Content-Type: image/png | image/webpETag: W/"..."(weak ETag for cache validation)Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=900
Response (304): If If-None-Match matches the ETag.
Response (404): No logo uploaded yet.
Response (400): Invalid address.
Caching: 5-minute cache; weak ETag.
activity.ts
GET /activity
Summary: Paginated activity log (swaps, LP events, staking, etc.).
Query params:
type(enum: swap | add_liquidity | remove_liquidity | farm_stake | farm_unstake | farm_harvest | vault_stake | vault_unstake | vault_claim | deploy_token | motofun_launch | motofun_trade | motofun_graduation, optional)status(enum: pending | completed | failed | cancelled | stalled, optional)user(address, optional)venue(enum: motoswap | motofun | all, defaultall) - restricts the feed to one venue's kindscursor(opaque string, optional) - pagination cursor
Response (200):
{
"items": [
{
"transactionId": "0xabcd...",
"logIndex": 376,
"user": "0x...",
"kind": "farm_stake",
"status": "completed",
"timestamp": 1789726391,
"blockNumber": 26003714,
"data": {
"ctx": { "pid": 0 },
"legs": [
{ "token": "0xad1a21f61d653c6101b92c335f61140459c81c79", "amount": "3427820391788993158", "symbol": null, "decimals": 18 }
],
"usdValue": "20.844042"
}
}
],
"nextCursor": "ZXlKcFpDSTZOREkyTURkOQ.WZ6-aoIxCHpYZLEc",
"hasMore": true
}Response fields:
kind: string- activity typestatus: string- transaction statusdata: any- event-specific JSON (schema varies by kind, and the field can be absent). Swap and farm rows carryctx(for examplepairAddressorpid),legs[]: { token, amount, symbol, decimals }with Wei-string amounts, and a decimal-dollarusdValue
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Pagination: Keyset cursor, 25 items/page. Scope-bound to filters; changing filters invalidates the cursor.
Error responses:
- 400 - bad request (invalid filter)
- 403 - invalid/tampered cursor
Notes:
- Cursor is HMAC-signed and scope-bound to endpoint + filters.
- Empty result is never 404; returns
[].nextCursorisnullon the last page. - The feed carries swaps on every pair the indexer tracks, Motoswap and Uniswap V2 both, plus farm stakes, unstakes and harvests.
- The three
motofun_*kinds and themotofunvenue are reserved for a separate venue. On a deploy where it is off,venueis forced tomotoswapand those kinds are never served.
analytics.ts
GET /analytics/overview
Summary: Protocol TVL, volume, and fees for a time window.
Query params:
window(24h | 7d | 30d | all, default 24h)rank(tvl | volume, default tvl) - unused in response
Response (200):
{
"totals": {
"tvlUsd": "5000000.25",
"volumeUsd": "1000000.5",
"feesUsd": "10000.005"
},
"motofun": {
"volumeUsd": "20000.75",
"feesUsd": "240.009"
}
}Response fields:
totals: { tvlUsd, volumeUsd, feesUsd }- DEX onlymotofun: { volumeUsd, feesUsd }- OPTIONAL; absent (not zeroed) when the deploy has that flag off, so a flag-off response is byte-identical to one from before the field existed
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: A window of all includes the protocol's entire history. totals covers Motoswap pairs only, so a window with no Motoswap volume reads "0" across the board even while /dex/pools also lists tracked Uniswap V2 pairs.
GET /stats/series
Summary: Time-series TVL / volume / fees.
Query params:
metric(tvl | volume | fees, default tvl)range(24h | 7d | 30d, default 30d)
Response (200):
{
"buckets": [
{
"t": 1789725600,
"value": "5000000.25"
}
],
"changePct": 5.3
}Response fields:
buckets: [{ t, value }]- hourly buckets, oldest→newestt: number- bucket start time (Unix seconds, hour boundary)value: string- decimal-dollar (TVL is gauge/LOCF; volume/fees are additive)changePct: number | null- % change last bucket vs first non-zero; null if all zeros
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: TVL is a gauge (last-observation-carry-forward); volume/fees are additive sums. No 404; empty series returns [].
GET /analytics/revenue/series
Summary: Swap fee revenue by category over time: LP, creator, staking, Rakeback, buyback and burn.
Query params:
range(7d | 30d | 90d | 1y, default 30d)bucket(day | epoch, default day) - calendar day or 7d rakeback epoch
Response (200):
{
"bucket": "day",
"buckets": [
{
"t": 1789689600,
"total": "820",
"lp": "300",
"creator": "20",
"staking": "200",
"rakeback": "200",
"buybackBurn": "100"
}
]
}Response fields:
t: number- bucket-start Unix secondstotal: string- sum of the five legs below (decimal-dollar string)lp: string- the one computed leg: 0.30% of measured volume over the bucketcreator: string- measured creator fees (creator_fee_events)staking, rakeback, buybackBurn: string- per-bucket sums of the measured Collector distribution events, one derivation each; nothing is estimated from a weight ratio
There is no protocolFee field and no treasury field. total is what was distributed to participants, not the venue's whole take.
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: Every leg but lp is a write-time USD snapshot, never re-priced. Bucket boundaries are UTC midnight (day) or the Rakeback epoch, which rolls Sunday 00:00 UTC (epoch).
GET /analytics/revshare/series
Summary: MOTO staker rewards distributed over time.
Query params:
range(7d | 30d | 90d | 1y, default 30d)bucket(day | epoch, default day)
Response (200):
{
"bucket": "day",
"buckets": [
{
"t": 1789689600,
"value": "175"
}
]
}Response fields:
value: string- decimal-dollar of actual distributions (from vault_reward_events)
Notes: Write-time USD snapshots; null prices are dropped. Revshare differs from the theoretical split due to distribution timing and Collector re-weighting.
GET /analytics/creator-fees/series
Summary: Creator fee accruals to token creators over time.
Query params:
range(7d | 30d | 90d | 1y, default 30d)bucket(day | epoch, default day)
Response (200): Same shape as revshare series.
Notes: From creator_fee_events (FeeRouter.CreatorFee); write-time USD snapshots.
GET /analytics/breakdown
Summary: Lifetime measured revenue payouts.
Response (200):
{
"rakebackAccruedUsd": "50000.25",
"revshareToStakersUsd": "40000.5",
"creatorPayoutsUsd": "5000.75"
}Response fields:
rakebackAccruedUsd, revshareToStakersUsd, creatorPayoutsUsd: string- real measured lifetime sums over the same ledgers the windowed series bucket
Caching: Cache-Control: public, max-age=30, s-maxage=30, stale-while-revalidate=90 (CRON tier).
Notes: The former protocolFeeUsd and split fields were removed: they were a second, derived allocation of the same money and disagreed with the measured ledgers.
dex.ts
GET /dex/pools
Summary: Paginated pool directory, sortable by TVL/volume/fees.
Query params:
sort(tvl | volume24h | fees24h, default tvl)dir(asc | desc, default desc)cursor(opaque string, optional)
Response (200):
{
"items": [
{
"pairAddress": "0x...",
"token0": "0x...",
"token1": "0x...",
"reserve0": "133292018598175373111408005",
"reserve1": "175053998838797275747",
"lastUpdated": 1789726715,
"tvlUsd": "878879.607649",
"volume24hUsd": "1305195.246871",
"fees24hUsd": "3915.584766",
"feeAprBps": 16261,
"tvlDeltaPct": 5.2,
"volume24hDeltaPct": -3.1,
"fees24hDeltaPct": 2.0,
"volume30dUsd": "0"
}
],
"nextCursor": null,
"hasMore": false
}Pagination: Keyset cursor, 100 items/page. Cursor scope-bound to `{ sort, dir }`.
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Error responses:
- 400 - bad request
- 403 - invalid cursor
Notes: Nulls rank last in all sort orders (e.g., pools with no APR sort to the bottom). volume30dUsd is unique to the list (not on detail). nextCursor is null when hasMore is false. The list holds every pair the indexer tracks, Motoswap pairs and the Uniswap V2 pairs the launch farm used.
GET /dex/pools/{pair}
Summary: Single pool detail + reserves + current stats.
Path params:
pair- pair/LP token address
Response (200):
{
"pairAddress": "0x...",
"token0": "0x...",
"token1": "0x...",
"reserve0": "133292018598175373111408005",
"reserve1": "175053998838797275747",
"lastUpdated": 1789726715,
"tvlUsd": "878879.607649",
"volume24hUsd": "1305195.246871",
"fees24hUsd": "3915.584766",
"feeAprBps": 16261,
"tvlDeltaPct": 5.2,
"volume24hDeltaPct": -3.1,
"fees24hDeltaPct": 2.0
}Response (404): Pair not indexed.
Response (200, empty pool): If the pair is real but has never been priced:
{
"pairAddress": "0x...",
"token0": "0x...",
"token1": "0x...",
"reserve0": "0",
"reserve1": "0",
"lastUpdated": 1723456789,
"tvlUsd": "0",
"volume24hUsd": "0",
"fees24hUsd": "0",
"feeAprBps": null,
"tvlDeltaPct": null,
"volume24hDeltaPct": null,
"fees24hDeltaPct": null
}Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: Empty/drained pools return genuine $0 state instead of 404 (so LP panels can add/remove liquidity). Reserves are authoritative from pair_stats; USD aggregates are "0" per zero-metric rule; deltas/APR stay null (uncomputable).
GET /dex/pools/{pair}/chart
Summary: Price candles, volume, fees, and deltas for a pool.
Path params:
pair- pair address
Query params:
range(24h|7d|30d|1y, default24h) - anything else fails validation
Bucket width follows the range and is echoed back as bucketMinutes: 24h serves one-minute
candles, 7d 15-minute candles, 30d hourly candles and 1y daily candles. Every range arrives
whole (at most 1,441, 673, 721 and 366 rows). Rows are sparse: a bucket with no trade has no candle.
minute is a number, the bucket's start in unix minutes, not seconds. Multiply by 60 to get a
timestamp. Re-bucket on this field and bucketMinutes rather than assuming a fixed spacing.
Response (200):
{
"spot": "0.000001313311934801",
"tvlUsd": "878879.607649",
"change5mPct": 0.5,
"change30mPct": 1.2,
"change6hPct": -2.1,
"change24hPct": 5.0,
"volume5mUsd": "2769.116479",
"volume1hUsd": "26792.557773",
"volume24hUsd": "1305195.246871",
"fees24hUsd": "3915.584766",
"swapCount24h": 1955,
"bucketMinutes": 1,
"candles": [
{
"minute": 29828778,
"open": "2.4",
"high": "2.6",
"low": "2.3",
"close": "2.5"
}
]
}Pool-chart candles carry OHLC only - no per-candle volumeUsd (the window
volumes above are the volume surface).
Response (200, empty pool): spot: null, tvlUsd: null, volumes "0", deltas null, candles: [].
Response (404): Pair not indexed.
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: Spot ratio is token1 / token0 serialized as a decimal string. Empty/drained pools return a genuine empty chart (not 404). The tracked Uniswap V2 pairs answer with spot, tvlUsd and the window volumes filled, and with candles: [] and null deltas, because candles come from Motoswap pairs.
GET /dex/pools/{pair}/swaps
Summary: Paginated swaps in this pool.
Path params:
pair- pair address
Query params:
page(int, 1-200, default 1)
Response (200): items[] rows have the same fields as /tokens/{address}/swaps (without the optional poolCount and outerInput), but the envelope differs:
{ "items": [ ], "page": 1, "totalPages": 200, "total": 10000 }The list reaches back 200 pages, so total tops out at 10000 on a busy pool.
Pagination: Page-numbered, 50 swaps/page, page 1 to 200.
GET /dex/pools/holder/{address}/pairs
Summary: Pairs where this wallet ever received LP (auto-detect candidate set).
Path params:
address- wallet address
Response (200):
{
"pairs": [
{
"pairAddress": "0x...",
"token0": "0x...",
"token1": "0x...",
"reserve0": "1000000000000000000",
"reserve1": "500000000000000000"
}
]
}Response (200, empty): { "pairs": [] }.
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: Read from LP transfer events, so it is always live. Hard-capped at 500 distinct pairs to prevent unbounded queries. Call balanceOf on each pair to find the wallet's active positions.
explore.ts
GET /explore/farms
Summary: Paginated farm ranking by TVL/APR/multiplier.
Query params:
sort(tvl | apr | multiplier, default tvl)dir(asc | desc, default desc)cursor(opaque string, optional) - integer rank cursor
Response (200):
{
"farms": [
{
"rank": 2,
"chef": "0x51e648f08a9a08a724591938d9cbc483c809aca4",
"pid": 0,
"isSingleSided": false,
"token0": "0xbd965230588eaa536de6aa45e8ebbc01638535e0",
"token1": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"lpToken": "0xad1a21f61d653c6101b92c335f61140459c81c79",
"rewardSymbol": "MOTO",
"multiplier": 800,
"tvlUsd": "271155.767887",
"aprBps": 707893
}
],
"nextCursor": null,
"hasMore": false
}Response fields:
rank: number- per-page rank (1, 2, 3, …)chef: string- the farming contract this pool lives in. Apidis unique only within one chef, so address a farm as(chef, pid)pid: number- pool ID inside that chefisSingleSided: boolean- true if staking single assettoken0, token1: string | null- the LP's tokens; bothnullon a single-sided farmlpToken: string- staked token address (pair or single token)multiplier: number- allocPoint (used to compute APR)tvlUsd: string- computed TVL (decimal-dollar string);"0"if unpricedaprBps: number | null- projected APR in basis points, or null if uncomputable
Pagination: In-memory rank cursor, 100 items/page. Nulls (APR) rank last.
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: APR is null when MOTO price is unset or TVL is zero. The farm set is small/curated; ranking is done in-memory. The array is named farms, not items. The launch farm's pools are Uniswap V2 LP tokens plus single-sided MOTO.
GET /explore/tokens
Summary: Paginated token ranking by TVL/volume/price.
Query params:
sort(tvl | volume24h, default tvl) - anything else fails validationdir(asc | desc, default desc)cursor(opaque string, optional) - composite sort key + token cursor
Response (200):
{
"items": [
{
"rank": 1,
"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"priceUsd": "2510.31",
"ethPrice": "1000000000000000000",
"change1hPct": 2.5,
"change1dPct": 5.0,
"volume24hUsd": "7810664.29",
"tvlUsd": "28315196.48",
"spark": [2498.12, 2503.4, 2510.31]
}
],
"nextCursor": null,
"hasMore": false
}Response fields:
rank: number- per-page rankpriceUsd: string | null- decimal-dollar price string or nullethPrice: string | null- Wei per token (full precision) or nullchange1hPct, change1dPct: number | null- percent deltas or nullvolume24hUsd, tvlUsd: string- fixed-format strings (e.g.,"1234567.89")spark: number[]- 7d sparkline of raw prices (not normalized);[]when there is no price history
Pagination: Composite keyset cursor (sort key + token address), 100 items/page. Bound to `{ sort, dir }`.
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: Change % is null if no historical candle. change1hPct and change1dPct are null and spark is [] on a row with no Motoswap candle history, because candles come from Motoswap pairs.
GET /explore/creators
Summary: Creators ranked by lifetime creator-fee earnings.
Query params:
venue(all | motoswap | motofun, default all)cursor(opaque string, optional)
Response (200):
{
"creators": [
{
"rank": 1,
"creator": "0x...",
"totalUsd": "1250.5",
"tokenCount": 3,
"unpricedTokens": 0
}
],
"nextCursor": null,
"hasMore": false,
"updatedAt": 1789726576
}Response fields:
totalUsd: string- lifetime creator fees, decimal-dollar stringtokenCount: number- the creator's tokens with a positive priced total, not tokens createdunpricedTokens: number- tokens with at least one unpriced fee leg; when above 0,totalUsdis a lower boundupdatedAt: number | null- Unix seconds of the last ranking refresh, or null if it has never run
Caching: Cache-Control: public, max-age=30, s-maxage=30, stale-while-revalidate=90 (CRON tier).
Error responses: 400 (bad request), 403 (invalid cursor).
Notes: The array is named creators. It answers empty when no registered token has accrued a creator fee.
farms.ts
GET /farms
Summary: Full farm directory with global chef state (unpaginated).
Response (200):
{
"chainId": 1,
"currentBlock": 26002074,
"pools": [
{
"chef": "0x51e648f08a9a08a724591938d9cbc483c809aca4",
"pid": 0,
"lpToken": "0xad1a21f61d653c6101b92c335f61140459c81c79",
"allocPoint": 800,
"depositFeeBps": 0,
"totalStaked": "41669322746313790009193",
"isSingleSided": false,
"pair": {
"pairAddress": "0xad1a21f61d653c6101b92c335f61140459c81c79",
"token0": "0xbd965230588eaa536de6aa45e8ebbc01638535e0",
"token1": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"reserve0": "125435543370865961114919638",
"reserve1": "177491136820610639233"
},
"displayName": null,
"lastUpdated": 1789413083,
"tvlUsd": "258219.642354",
"aprBps": 782785,
"volume24hUsd": "993278.337482",
"fees24hUsd": "2979.834289"
}
],
"global": {
"id": 1,
"totalAllocPoint": "1100",
"initialPeriodReward": "248015873015873015873",
"totalEmission": "300000000000000000000000000",
"emissionStart": 1789415087,
"emissionEnd": 1790624687,
"currentRewardPerSecond": "248015873015873015873",
"lastUpdated": "YYYY-MM-DDTHH:mm:ss.sssZ"
},
"motoDistributed": "72321428571428571428566800"
}Response fields:
pools[].chef: string- the farming contract the pool lives in. SenduserInfo/pendingRewardreads and deposits to THIS address, and pass it toGET /pnl/{address}/farm/{chef}/{pid}pools[].pair: object | null- null if single-sided or unindexeddisplayName: string | null- human-readable pool name or nullglobal: object | null- null if not yet initializedvolume24hUsd, fees24hUsd: string- the staked pair's 24h figures;"0"on a single-sided farm
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: Unpaginated (governance-curated set, one add() per pool). FE filters client-side.
holdings.ts
GET /holdings/tokens
Summary: Curated token set with current + 24h-ago prices (for user balance displays).
Response (200):
{
"tokens": [
{
"address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"decimals": 18,
"priceUsd": "2510.31",
"ethPrice": "1000000000000000000",
"priceUsd24hAgo": "2498.12"
},
{
"address": "0xbd965230588eaa536de6aa45e8ebbc01638535e0",
"decimals": 18,
"priceUsd": "0.003296",
"ethPrice": "1313369318829",
"priceUsd24hAgo": null
}
]
}Response fields:
decimals: number- token decimals (RPC-resolved or fallback 18)priceUsd: string | null- decimal-dollar price string or null (unknown)ethPrice: string | null- WETH per whole token, scaled by 1e18 (a Wei-style integer string), or null. Full precision; prefer it overpriceUsdfor low-priced tokenspriceUsd24hAgo: string | null- LOCF from 24h-ago candle, or null.nullon a row with no candle 24 hours back, because it comes from candles
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes:
priceUsd: nullrenders as a dash on the FE (not $0).priceUsd24hAgo: nullmeans no historical data; FE shows no delta.- Bounded set: WETH, USDC, USDT, WBTC and MOTO, plus graduated moto.fun coins that have a price.
motocats.ts
GET /motocats/{address}
Summary: Motocat token IDs held in a wallet (not staked).
Path params:
address- wallet address
Response (200):
{
"tokenIds": ["1", "42", "1337"]
}Response (200, no cats): { "tokenIds": [] }.
Response (400): Invalid address.
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes:
- "Owned" = current holder (latest transfer's
to). - Staked cats are held by the staking contract, so excluded.
- Read from
motocat_nft_transfer_events(write-path table, live).
launchpad.ts
GET /launchpad/recent
Summary: 12 most recent token launches (newest first).
Response (200):
{
"items": [
{
"address": "0x...",
"symbol": "NEWTOKEN",
"name": "New Token",
"deployer": "0x...",
"timestamp": 1790600000,
"origin": "motofun",
"graduatedAt": null,
"priceUsd": "0.000012",
"liquidityUsd": "50000.25"
}
]
}Response fields:
timestamp: number- launch time (Unix seconds)origin: "deployer" | "motofun"- which launch surface created the token.deployermarks coins from the retired launch contractgraduatedAt: number | null- Unix seconds of graduation for a moto.fun coin, or nullpriceUsd: string | null- decimal-dollar price string or null (not yet priced)liquidityUsd: string | null- sum of both pair TVLs (MOTO + quote), or null
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: Chronological (newest first); never ranked. Priced/liquidity are the first values from the stats sinks (null if not yet computed). Answers { "items": [] } when nothing matches the window.
GET /launchpad/token/{address}
Summary: Single launched token detail + deployer's other launches.
Path params:
address- launched token address
Response (200):
{
"launch": {
"address": "0x...",
"symbol": "NEWTOKEN",
"name": "New Token",
"deployer": "0x...",
"timestamp": 1790600000,
"origin": "motofun",
"graduatedAt": null,
"priceUsd": "0.000012",
"liquidityUsd": "50000.25"
},
"otherLaunches": [ ],
"otherLaunchesTotal": 5
}Response (200, not a launched token): { "launch": null, "otherLaunches": [], "otherLaunchesTotal": 0 }. There is no 404 on this route.
Response (400): Invalid address.
Response fields:
launch: LaunchItem | null- the requested token, or null when the address was not launched hereotherLaunches: LaunchItem[]- up to 8 other launches by the same deployer (newest first)otherLaunchesTotal: number- true total of the deployer's OTHER launches (to render "N other launches" copy)
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: LaunchItem shape is same as /launchpad/recent.
pnl.ts
GET /pnl/{address}/token/{token}
Summary: Per-wallet realized P&L for a token.
Path params:
address- wallet addresstoken- token address (cannot be a quote asset)
Response (200):
{
"token": "0x...",
"partial": false,
"buys": 5,
"sells": 3,
"unitsBought": "286415823120588715703305",
"unitsSold": "127623434821278976299310",
"boughtUsd": "975.981157",
"soldUsd": "431.269881",
"costOfSoldUsd": "434.885427",
"realizedPnlUsd": "-3.615546",
"realizedPnlBps": -83,
"firstBuyTimestamp": 1789446107,
"lastSellTimestamp": 1789726391
}Response fields:
partial: boolean- true if unpriced rows excluded (FE must NOT render a P&L number)unitsBought, unitsSold: string- Wei (raw units)*Usd: string- decimal-dollar string;realizedPnlUsdis signedrealizedPnlBps: number | null- basis points gain/loss, signedfirstBuyTimestamp, lastSellTimestamp: number | null- Unix seconds
Response (400): Invalid address/token, or token is a quote asset.
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes:
- Quote assets (USDC, WETH, USDT, MOTO) cannot be analyzed, because P&L is measured in them.
partial: truemeans the FE should show a warning ("data incomplete, unpriced swaps excluded").- Covers MOTO trades on the tracked Uniswap V2 pairs as well as Motoswap pairs.
GET /pnl/{address}/farm/{chef}/{pid}
Summary: Per-wallet staking P&L for a farm.
Path params:
address- wallet addresschef- the farming contract address, from the pool'scheffield onGET /farmspid(int) - pool ID inside that chef
The older two-segment form, /pnl/{address}/farm/{pid}, no longer exists and answers 404.
Response (200):
{
"chef": "0x51e648f08a9a08a724591938d9cbc483c809aca4",
"pid": 0,
"partial": false,
"stakes": 11,
"unstakes": 0,
"harvests": 21,
"stakedInUsd": "2811.39529",
"unstakedOutUsd": "0",
"harvestedUsd": "1722.881042",
"harvestedMotoWei": "509945373012941070495206",
"stakeUnitsWei": "469278446511578501145",
"netStakeUnits": "469278446511578501145",
"firstStakeTimestamp": 1789446107
}Response fields:
stakedInUsd, unstakedOutUsd, harvestedUsd: string- decimal-dollar strings, frozen at write timeharvestedMotoWei?: string- MOTO harvested, in Wei. OptionalstakeUnitsWei?: string- LP (or single token) units staked, in Wei. OptionalnetStakeUnits: string- stakes minus unstakes, in WeifirstStakeTimestamp: number | null- Unix seconds, or null for a wallet that never staked
Response (400): Invalid address, chef or pid.
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes:
partial: trueif rows lacked prices.- All USD values are frozen write-time snapshots.
- A wallet with no position answers 200 with zero counts, not 404.
points.ts
GET /points/leaderboard
Summary: Top 100 wallets by total points (the daily-drop projection).
Response (200):
{
"rows": [
{
"rank": 1,
"address": "0x...",
"totalPoints": 10000,
"swapCount": 500
}
]
}Response fields:
totalPoints: number- integer points (1/1000 milli precision)- All figures are from the daily-dropped snapshot (not live ledger)
Caching: Cache-Control: public, max-age=30, s-maxage=30, stale-while-revalidate=90 (CRON tier - recomputed at the daily drop / settlement, not per block).
Notes: Excludes wallets with 0 dropped points. Only the top 100 are listed. An empty board answers { "rows": [] }.
GET /points/{address}
Summary: Per-wallet points, rank, season state, recent swaps.
Path params:
address- wallet address
Response (200):
{
"address": "0x...",
"season": {
"catsWorking": 2
},
"totalPoints": 5000,
"swapCount": 250,
"referralPoints": 500,
"referralPointsDisplay": "500",
"rank": 42,
"percentile": 42,
"totalRanked": 10000,
"firstSwapAt": 1789446107,
"referral": { "linkable": false, "reason": "already_scored" },
"droppedAt": 1789689600,
"todayActions": 12,
"recentSwaps": [
{
"txHash": "0x...",
"logIndex": 0,
"timestamp": 1723456789,
"pair": "0x...",
"tokenIn": "0x...",
"tokenOut": "0x...",
"amountIn": "1000000000000000000",
"amountOut": "500000000000000000"
}
]
}Response fields:
season.catsWorking: number- bucketed count of the cats this wallet has staked inMotocatStaking. Cats held in the wallet do not count, and the figure is bucketed rather than exact so the multiplier cannot be read straight out of a probetotalPoints: number- integer points from the daily dropreferralPoints: number- 10% of referees' pointsreferralPointsDisplay: string- the same figure as display text:"0", a"<N"form for a small non-zero total, or the number as text. Render this onereferral: { linkable, reason }- whether a referral binding can still be made for this wallet.reasonisok,already_linkedoralready_scored. It is the same checkPOST /referrals/attributerefuses on.linkable: truesays nothing about the referrer side (self-referral and cycles are still refused)rank: number | null- 1-indexed rank on leaderboard, or null if unrankedpercentile: number | null- 0-100 percentile, or nulltotalRanked: number | null- total ranked wallets, or nullfirstSwapAt: number | null- Unix seconds of first transaction, or nulldroppedAt: number | null- 00:00-UTC boundary of last drop, or nulltodayActions: number- count of all swaps since the last drop (includes unscored swaps)recentSwaps: array- last 20 swaps (newest first)
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Error responses: 400 (invalid address).
Notes:
- All figures are from the daily-dropped projection (not live ledger).
todayActionscounts all swaps.- A wallet with no history answers 200 with zeros and nulls, not 404. Before the first daily drop
totalPointsis0andrank,percentile,totalRankedanddroppedAtarenull, whilefirstSwapAt,todayActionsandrecentSwapsalready fill from swaps on the tracked pairs.
GET /points/rules
Summary: Public season config: V_min, referral rate, and copy.
Response (200):
{
"vMinUsd": null,
"referralRateBps": 1000,
"copy": {
"story": "...",
"cats": "...",
"catFootgun": "...",
"weeklyBonus": "...",
"referral": "...",
"wash": "...",
"settlement": "..."
}
}Response fields:
vMinUsd: string | null- the published weekly volume floor (decimal-dollar string), or null while unsetreferralRateBps: number- 10% = 1000 basis pointscopy: { … }- seven display strings for a rules screen. The keys are stable; the wording changes without notice, so render the strings and never parse them
Caching: Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=900 (STATIC tier).
Notes: The two numbers this endpoint returns are the only published season parameters (they drive user behaviour). The scoring model and all of its other parameters are not published. vMinUsd is null while unset.
GET /points/weeks
Summary: Settled weeks and their windows.
Response (200):
{
"weeks": [
{
"week": 0,
"weekStart": 1723420800,
"weekEnd": 1724025600
}
]
}Response fields:
week: number- settled week indexweekStart, weekEnd: number- window bounds (Unix seconds)
Response (200, no weeks): { "weeks": [] }.
Caching: Cache-Control: public, max-age=30, s-maxage=30, stale-while-revalidate=90 (CRON tier - recomputed at the daily drop / settlement, not per block).
GET /points/leagues
Summary: Current week's leagues: wallets grouped into small leagues.
Response (200):
{
"week": 5,
"leagueSize": 25,
"leagues": [
{
"league": 0,
"members": [
{
"rank": 1,
"address": "0x...",
"points": 5000,
"movement": 2
}
]
}
]
}Response (200, no leagues): { "week": 0, "leagueSize": 25, "leagues": [] }.
Response fields:
week: number- settled weekleagueSize: number- target members per leaguemembers[].movement: number- rank change from last week
Caching: Cache-Control: public, max-age=30, s-maxage=30, stale-while-revalidate=90 (CRON tier - recomputed at the daily drop / settlement, not per block).
Notes: Leagues are assigned at each weekly settlement. Empty until the first settlement.
GET /points/fees
Summary: Protocol fee dashboard (revenue breakdown).
Response (200):
{
"totalFeesUsd": "105730.88584",
"fees24hUsd": "23522.861098",
"fees7dUsd": "105730.88584",
"swapCount": 40731,
"split": [
{ "bucket": "lp", "weight": 0, "shareBps": 30 },
{ "bucket": "creator", "weight": 0, "shareBps": 30 },
{ "bucket": "staking", "weight": 2, "shareBps": 20 },
{ "bucket": "rakeback", "weight": 2, "shareBps": 20 },
{ "bucket": "buybackBurn", "weight": 1, "shareBps": 10 }
],
"burnAddress": "0x...",
"treasuryAddress": "0x...",
"collectorAddress": "0x...",
"feeBps": { "lp": 30, "protocol": 70, "creator": 30, "total": 100 }
}The example shows the DEX fee configuration. Every number in it is settable on chain, so read the
live values rather than trusting the example: a zero protocol share means the FeeRouter charges
nothing for the current phase, and the three protocol rows then report shareBps: 0.
Response fields:
feeBps.totalislp + protocolonly, so the base take on every trade is 100 bps (1.00%). The creator fee is NOT in that totalsplit[]has five rows:lp,creator,staking,rakeback,buybackBurn. The treasury bucket is not servedsplit[].weightis theCollectorbucket weight.lpandcreatorreport0because neither is aCollectorbucketsplit[].shareBpsfor the three protocol rows isprotocolFeeBps × weight / 7, so 20 / 20 / 10 bps against a 70 bps protocol fee
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes:
- The creator fee is a separate, optional 0.30% that applies only to trades on creator-fee-enabled tokens, charged on the same quote leg on top of the base 100 bps. A one-hop trade with one creator-fee token totals 1.30%, and every other one-hop trade is 1.00%. Each extra hop adds its own 0.30% pair fee. A trade with creator-fee tokens at both ends always routes through a quote asset, so it pays two pair fees, the protocol fee once and both creator fees: about 1.89% all-in. Its
shareBpsis the rate on those trades, not a slice of every trade, which is why the rows do not sum tofeeBps.total totalFeesUsdand the windowed figures measure the creator leg rather than deriving it from volume, precisely because it applies per registered token- Some internal fee-accounting figures were retired from the public dashboard and are not served here
GET /points/pair/{pair}
Summary: Pool classification for points scoring (public label only).
Path params:
pair- pair address
Response (200):
{
"pair": "0x...",
"deployer": "0x...",
"poolLabel": "standard"
}Response (200, burn-verified):
{
"pair": "0x...",
"deployer": "0x...",
"poolLabel": "burn-verified"
}Response (404): Unclassified pair.
Response (400): Invalid address.
Response fields:
poolLabel: "standard" | "burn-verified"- the only public classification for a pair. No other scoring attribute is exposed.
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
portfolio.ts
GET /portfolio/{address}
Summary: Portfolio existence check (resolves address).
Path params:
address- wallet address
Response (200):
{ "address": "0x..." }Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: This is a thin route that only echoes the validated address. Per-user data (balances, prices) is chain-direct, not DB-mirrored.
GET /portfolio/{address}/lp-fees
Summary: Per-pair cumulative LP fees earned (Model A).
Path params:
address- wallet address
Response (200):
{
"positions": [
{
"pairAddress": "0x...",
"feesEarnedUsd": "50000000",
"sinceTimestamp": 1723456000
}
]
}Response (200, no positions): { "positions": [] }.
Response fields:
feesEarnedUsd: string- cumulative fees (decimal-dollar, frozen snapshots + 1h flow)sinceTimestamp: number- when the position opened (first snapshot)
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: Positions are per-pair LP balances. Fees are computed from snapshots + hourly flow buckets.
GET /portfolio/{address}/lp-fees/{pair}
Summary: Per-position cumulative fee series (calm sparkline).
Path params:
address- wallet addresspair- pair address
Response (200):
{
"points": [
{
"timestamp": 1723456000,
"feesEarnedUsdCumulative": "1000000",
"positionValueUsd": "50000000"
}
]
}Response (200, no history): { "points": [] }.
Response fields:
feesEarnedUsdCumulative: string- running total (decimal-dollar string)positionValueUsd: string | null- frozen write-time snapshot (null if unpriced)
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes: One point per LP event (add/remove). Series is cumulative; sparkline is calm, not a live ticker.
rakeback.ts
GET /rakeback/stats
Summary: Global Rakeback epoch state.
Response (200):
{
"currentEpoch": 2958,
"nextEpoch": 2959,
"totalEpochs": 2959
}Caching: Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=900 (STATIC - immutable after epoch close).
Notes: An epoch is one week, Sunday 00:00 UTC to Sunday 00:00 UTC: currentEpoch = floor((now - 259200) / 604800), and nextEpoch = currentEpoch + 1. totalEpochs counts from a configured genesis epoch; while that is unset it counts from epoch 0. Do not read it as the number of Rakeback epochs paid.
GET /rakeback/{address}
Summary: Per-wallet rakeback rollup, per-leaf claim state with proofs, and epoch history.
Path params:
address- wallet address
Response (200):
{
"currentEpoch": 2958,
"nextEpoch": 2959,
"totalEpochs": 2959,
"swapsMade": 500,
"tokensTraded": 25,
"tokens": [
{
"token": "0x...",
"claimableNow": "500000000000000000",
"claimed": "100000000000000000",
"pending": "400000000000000000",
"expired": "0"
}
],
"epochs": [
{
"epoch": 2958,
"token": "0x...",
"status": "claimable",
"amount": "500000000000000000",
"index": 17,
"proof": ["0x...", "0x..."],
"root": "0x...",
"activatedAt": 1723456889,
"claimDeadline": 1726048889,
"usdValue": "500.00"
}
],
"epochRewards": [
{
"epoch": 2956,
"amounts": [
{
"token": "0x...",
"amount": "1000000000000000000",
"usdValue": "500.00"
}
]
}
]
}Response fields:
swapsMade: number- fee-paying trades counted off the accrual sinktokensTraded: number- distinct tokens the wallet swappedtokens[]- per-token rollup of the wallet's leaves by lifecycle status. A leaf is all-or-nothing, so each bucket is a straight sum of leaf amounts in that statusepochs[]- one row per published(epoch, token)leaf the wallet holds, carrying the merkle preimage (index,amount,proof,root) thatRakebackV2.claimtakesepochs[].status-pending(posted, not yet activated),claimable(activated, in window, unclaimed),claimed, orexpired(window passed, or the root was cancelled)epochs[].amount: string- the VESTED leaf amount in Wei, not the raw accrualepochs[].activatedAt, claimDeadline: number | null- Unix seconds; null while pendingepochs[].usdValue, epochRewards[].usdValue: string- write-time USD snapshot of that epoch's accrual, never re-pricedepochRewards[]- raw accrual plus frozen USD over the last 12 epochs, for the rewards-over-time chart
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier, per-user versioned on the wallet's swap and claim watermark, and on the weekly epoch roll).
Error responses: 400 (invalid address). 503 (indexer head lagging or missing - leaf statuses would be stale-claimable, so the route refuses rather than offering a claim the chain would reject).
Notes: Immutable past epochs are fetched via /rakeback/{address}/epoch/{epoch} (STATIC cache). Proofs alone are available at /rakeback/{address}/proof. A wallet with no epoch open to it yet answers with tokens: [], epochs: [] and twelve epochRewards rows whose amounts are empty. Epochs close Sunday 00:00 UTC and open to claim about a day later, so that is every wallet until the first root is posted.
GET /rakeback/{address}/epoch/{epoch}
Summary: One epoch's leaves for a wallet (immutable once the epoch settles).
Path params:
address- wallet addressepoch(int) - epoch number, bounded to int32
Response (200):
{
"epoch": 2956,
"tokens": [
{
"epoch": 2956,
"token": "0x...",
"status": "claimed",
"amount": "500000000000000000",
"index": 17,
"proof": ["0x...", "0x..."],
"root": "0x...",
"activatedAt": 1722247289,
"claimDeadline": 1724839289,
"usdValue": "500.00"
}
]
}Response fields: tokens[] carries the same leaf rows as epochs[] on GET /rakeback/{address}, scoped to one epoch.
Caching: Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=900 (STATIC - past epochs are immutable; the current epoch folds the user watermark).
Error responses: 400 (invalid address or epoch). 503 (indexer head lagging or missing).
GET /rakeback/{address}/proof
Summary: Merkle claim proofs for every published leaf this wallet holds.
Path params:
address- wallet address
Response (200):
{
"items": [
{
"epoch": 2958,
"token": "0x...",
"index": 17,
"amount": "1000000000000000000",
"proof": ["0x...", "0x..."],
"root": "0x..."
}
]
}Response fields:
index: number- the leaf's global 0-based position in its epoch treeamount: string- the leaf amount in Wei; the vest is already baked inproof: string[]- sorted-pair sibling path verifying againstrootroot: string- the epoch root this proof verifies against
Together with epoch, token and the wallet address, these are the exact
arguments RakebackV2.claim takes.
Caching: Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=900 (STATIC - a posted root is
immutable). The version tag folds the address with a roots watermark, so it
flips only when a new tree the wallet appears in is published.
Error responses: 400 (invalid address).
Notes:
- An unknown wallet returns an empty
itemsarray, not a 404. - The same rows also ride
GET /rakeback/{address}, so a client that already loaded the panel state does not need this call.
referrals.ts
POST /referrals/attribute (wallet-signed)
Summary: Bind a referral (referee → referrer); requires EIP-191 signature from referee.
The referee signs this exact text with personal_sign. Both addresses are lowercased, <chainId> is the chainId from GET /config, and the blank lines are part of the message:
Activate your Motoswap referral link.
This is a free signature. It does not send a transaction, spend gas, or approve any funds. It links your wallet to the person who referred you.
Referred by: <referrer>
Your wallet: <referee>
motoswap-referral:<referee>:<referrer>:<chainId>Request body:
{
"referee": "0x...",
"referrer": "0x...",
"signature": "0x...",
"source": "twitter"
}Request fields:
referee, referrer: string- 0x-addressessignature: string- 0x-hex EIP-191 personal_sign of the referral messagesource: string- optional freetext (sanitized, max 256 chars)
Response (200, bound):
{
"bound": true,
"binding": {
"refereeAddress": "0x...",
"referrerAddress": "0x...",
"source": "twitter",
"boundAt": "2024-08-12T10:30:00Z"
}
}Response (200, already bound):
{
"bound": false,
"already": true,
"binding": { "..." : "..." }
}Response (400): { "bound": false, "error": "<reason>" }. Reasons include a self-referral or cycle, referee-already-active (the referee has already scored Points) and referrer-cap-reached. A malformed body answers { "error": "referee and referrer must both be 0x-addresses" }.
Response (403): { "bound": false, "error": "signature does not authorize this binding" }. EOA recovery failed and the ERC-1271 check did not pass.
Caching: no-store (authentication required).
Notes:
- First-touch binding is permanent; a referee can only have one referrer.
- Signature proves control of the referee address (EOA or smart account).
- Binding fails once the referee has scored Points (to prevent retroactive harvesting).
GET /points/{address}answers the same question ahead of time in itsreferralfield. - Free-text
sourceis sanitized at the zod layer (NUL byte protection).
GET /referrals/{address}
Summary: Referral summary for a wallet (referrer info + referee count + points).
Path params:
address- wallet address
Response (200):
{
"address": "0x...",
"referrer": "0x...",
"boundAt": "2024-08-12T10:30:00Z",
"refereeCount": 42,
"referralPoints": 5000,
"referralPointsDisplay": "5000",
"generatedAt": "YYYY-MM-DDTHH:mm:ss.sssZ"
}Response (200, no binding):
{
"address": "0x...",
"referrer": null,
"boundAt": null,
"refereeCount": 0,
"referralPoints": 0,
"referralPointsDisplay": "0",
"generatedAt": "YYYY-MM-DDTHH:mm:ss.sssZ"
}Response fields:
referrer: string | null- wallet's referrer (if bound)refereeCount: number- distinct referees (10% of their points accrue to referrer)referralPoints: number- 10% of referees' lifetime pointsreferralPointsDisplay: string- the same figure as display text (seeGET /points/{address})
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Error responses: 400 (invalid address).
stake.ts
GET /stake/vaults
Summary: Lock-up vault directory with TVL and projected APR.
Response (200):
{
"vaults": [
{
"vaultId": 0,
"label": "<string>",
"lockDuration": "<number, seconds>",
"rewardMultiplierBps": "<number>",
"tvl": "20789840112855468232743",
"projectedAprBps": null
}
],
"motoPriceUsd": "0.003296"
}The label, duration and multiplier values are elided above. Read them from the response.
Response fields:
vaultId: number- the lock-duration option, 0 to 3. These are options on oneMotoStakingposition, not four separate vaultslabel: string- human-readable namelockDuration: number- secondsrewardMultiplierBps: number- the stake-weight multiplier for the option, in bps. On chain,MotoStaking.boostBps(durationOption)stores the EXTRA on top of a 1x base, somultiplier = 1 + boostBps / 10000andrewardMultiplierBps = 10000 + boostBps. This field is a static value; governance can changeboostBps, and the chain is the source of truthtvl: string- Wei (total staked)projectedAprBps: number | null- annualized return basis points, or null if uncomputablemotoPriceUsd: string | null- decimal-dollar MOTO price string for FE display
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Notes:
- APR is null when MOTO price is unset or total effective stake is zero. Staking revenue is a share of swap fees, so a period with no fee inflow leaves it
null. - TVL is not per-user. Read a wallet's position from
MotoStakingon chain. - Read the label and duration from this route. Do not hardcode them.
moto.ts
GET /moto/overview
Summary: Where MOTO sits: staked per lock option, staked in total, and held in indexed pools.
Response (200):
{
"stakedByTier": [
{ "vaultId": 0, "staked": "20789840112855468232743" }
],
"stakedTotal": "1000164587070035394350762857",
"inMotoswapPools": "142861905011747233674438803",
"motoswapPoolCount": 31,
"pveLaunchCount": 0,
"motoPriceUsd": "0.003296"
}Response fields:
stakedByTier[]: { vaultId, staked }- Wei staked per lock option, four rowsstakedTotal, inMotoswapPools: string- WeimotoswapPoolCount: number- indexed pools that hold MOTO, Motoswap and Uniswap V2 bothpveLaunchCount: number- distinct launched tokens that sent a PvE allocationmotoPriceUsd: string | null- decimal-dollar MOTO price
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
swap.ts
GET /swap/candidates
Summary: Candidate swap path pairs (topology only, no reserves).
Query params:
tokenIn(address) - input tokentokenOut(address) - output token
Response (200):
{
"pairs": [
{
"address": "0x...",
"token0": "0x...",
"token1": "0x..."
}
]
}Response (200, no path): { "pairs": [] }.
Response (400): Invalid tokenIn or tokenOut.
Caching: Cache-Control: public, max-age=30, s-maxage=30, stale-while-revalidate=90 (CRON tier).
Notes:
- Candidates are edge topologies; FE uses live
getReserves()to quote. - Versioned on indexer head (no reserves, so versioning is automatic).
- No amount param - candidate set is amount-independent.
user.ts
GET /{address}/meta
Summary: User metadata (last activity block for cache invalidation).
Path params:
address- wallet address
Response (200):
{
"address": "0x...",
"lastActivityBlock": 6123456
}Response (200, no activity):
{
"address": "0x...",
"lastActivityBlock": 0
}Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Error responses: 400 (invalid address).
Notes: Used to version per-user caches. Block is updated on activity (referral binding, P&L calcs, etc.).
profile.ts
A wallet's public profile: a Motocat picture and an optional linked X handle. Every write is authorized by a signature from the wallet itself.
The profile object returned by GET /profile/{address}, POST /profile/pfp, POST /profile/x/link and POST /profile/x/unlink is the same shape:
{
"address": "0x...",
"motocatTokenId": "42",
"xHandle": "someone",
"xVerified": true
}motocatTokenId: string | null- decimal token id of the chosen Motocat, or nullxHandle: string | null- linked handle, or nullxVerified: boolean- false when no handle is linked
GET /profile/{address}
Path params: address - wallet address.
Response (200): the profile object. A wallet with no row still answers 200 with nulls.
Response (400): invalid address.
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier, versioned on the profile row's updated_at).
GET /profiles
Summary: Batch profile lookup for rendering a list of wallets.
Query params:
addresses(required) - comma-separated 0x-addresses, 1 to 120 of them
Response (200):
{ "profiles": { "0xabc...": { "address": "0xabc...", "motocatTokenId": null, "xHandle": null, "xVerified": false } } }Keyed by lowercased address; addresses with no row are simply absent from the map.
Response (400): empty list, more than 120 addresses, or a malformed address.
Caching: Cache-Control: public, max-age=15, s-maxage=15, stale-while-revalidate=45 (CRON_FAST tier - the only route on it).
POST /profile/pfp (wallet-signed)
Summary: Set or clear the wallet's Motocat picture.
Request body:
{ "address": "0x...", "tokenId": "42", "signature": "0x..." }tokenId: string | null- decimal token id, or null to clearsignature- signature over the pfp message for this address, token id and chain id
Response (200): the updated profile object.
Response (400): missing or malformed fields.
Response (403): signature does not authorize the change, or the wallet neither holds nor stakes that Motocat.
Caching: no-store.
GET /profile/x/status
Summary: Whether X linking is configured on this deploy, and the OAuth parameters a client needs to start the flow.
Response (200):
{
"enabled": false,
"clientId": null,
"authorizeUrl": "https://x.com/i/oauth2/authorize",
"scopes": "users.read tweet.read"
}clientId is null while linking is disabled. scopes is one space-separated string.
Caching: no-store.
POST /profile/x/link (wallet-signed)
Summary: Complete the X OAuth flow and attach the handle to the wallet.
Request body: address, signature, state, code, codeVerifier, redirectUri.
Response (200): the updated profile object with xVerified: true.
Response (400): missing or malformed fields.
Response (403): signature does not authorize the change.
Response (502): the code exchange with X failed.
Response (503): X linking is disabled on this deploy.
Caching: no-store.
POST /profile/x/unlink (wallet-signed)
Summary: Detach the linked handle.
Request body: address, signature.
Response (200): the updated profile object with xHandle: null, xVerified: false.
Response (400): missing or malformed fields.
Response (403): signature does not authorize the change.
Caching: no-store.
creator.ts
GET /creator/token/{token}
Summary: Token creator info and fee accruals for a specific token.
Path params:
token- token address
Response (200):
{
"token": "0x...",
"creator": "0x...",
"disabled": false,
"feesToDate": [
{
"quoteAsset": "0x...",
"amount": "1000000000000000000",
"usd": "2510.31"
}
]
}Response (200, no creator):
{
"token": "0x...",
"creator": null,
"disabled": false,
"feesToDate": []
}This is what a token with no recorded creator fees answers.
Response fields:
creator: string | null- creator address or nulldisabled: boolean- whether creator fees are disabledfeesToDate[].amount: string- WeifeesToDate[].usd: string | null- write-time USD or null (frozen at accrual)
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Error responses: 400 (invalid token).
GET /creator/{address}
Summary: Creator dashboard - lifetime earnings breakdown (token x quote asset).
Path params:
address- creator address
Response (200):
{
"address": "0x...",
"tokens": [
{
"token": "0x...",
"disabled": false,
"venue": "deployer",
"lastClaimAt": 1723456000,
"usdEarned": "500.00",
"quotes": [
{
"quoteAsset": "0x...",
"earned": "1000000000000000000",
"usdEarned": "500.00",
"swaps": 120,
"claimed": "500000000000000000",
"unclaimed": "500000000000000000"
}
]
}
],
"series": [
{ "day": 20310, "usdEarned": "12.50", "swaps": 4 }
],
"totalUsdEarned": "500.00"
}Response (200, no earnings): { "address": "0x...", "tokens": [], "series": [], "totalUsdEarned": "0" }.
Response fields:
address: string- the creator address (top-level identifier; there is nocreatorfield on this route)tokens[].disabled: boolean- whether creator fees are disabled for the tokentokens[].venue: "motofun" | "deployer"- which surface launched the token.deployermarks coins from the retired launch contract. Both accrue into the same vault, so this only labels the row; it never changes how the claim is madetokens[].lastClaimAt: number | null- Unix seconds of the last claim, or nulltokens[].usdEarned: string- sum over the token's quotesquotes[].earned: string- total accrued (Wei)quotes[].usdEarned: string- frozen write-time USD sum (never re-priced); unpriced accruals contribute 0quotes[].swaps: number- fee-paying swap count for this quote assetquotes[].claimed, unclaimed: string- claimed via on-chain withdrawal + remainingseries[]: { day, usdEarned, swaps }- ascending by daytotalUsdEarned: string- lifetime total across every token
Caching: Cache-Control: public, max-age=3, s-maxage=3, stale-while-revalidate=9 (BLOCK tier).
Error responses: 400 (invalid address).
pve.ts
GET /pve/token/{token}
Summary: A token's full PvE escrow state: received / distributed / escrowed, the credit (null until PveCredited), held-pending amount, claim totals, and per-creator receipts.
Response fields (strict): token, vault, received, distributed, escrowed, receivedUsd | null, receipts, distributions, lastBlock | null, credit: { amount, totalStaked, creditBlock } | null, heldPending, claimedTotal, claims, creators[]: { creator, received, receivedUsd | null, receipts }.
vault: Address is the vault instance holding this token's escrow (claims target it). Rows indexed before the column existed carry no vault of their own and fall back to the deploy's default PvE vault, so the field is always an address and never null.
Caching: BLOCK tier, token-scoped version.
GET /pve/creator/{address}
Summary: A creator's PvE receipts across their launched tokens.
Response fields: address, tokens[]: { token, received, distributed, escrowed, receivedUsd | null, receipts }, totalReceivedUsd | null.
Caching: BLOCK tier, address-scoped version.
GET /pve/wallet/{address}
Summary: A wallet's PvE claims: per token, the credit and this wallet's claim (null while unclaimed - eligibility and the live claimable amount come from PveVault.claimable on chain).
Response fields: address, tokens[]: { token, vault, credit: { amount, totalStaked, creditBlock }, creditTimestamp, claimed: { amount, txHash, blockNumber, timestamp } | null, ethPrice?: string | null, decimals?: number | null }. The optional ethPrice (WETH per whole token, scaled by 1e18) and decimals let a client value the share without a second call.
vault: Address is the vault instance that credited the token - the address claim(token) must target.
Caching: BLOCK tier, address-scoped version.
Notes: USD fields are frozen write-time snapshots (never re-priced). A wallet with no PvE credit answers with empty lists and zero amounts, and vault is the zero address.
Internal and non-public routes
The backend also mounts operational routes that are NOT for integrators: deep health checks, scheduled jobs, admin controls, an indexer webhook, compliance checks for the web app, and the web app's own RPC proxy. They are token-gated or rate-limited for the app and they change without notice. Anything the host answers outside this page is not a support commitment. Bring your own RPC endpoint for chain reads.
Summary
- 65 public endpoints on this page: 61 reads and 4 wallet-signed writes
- Cursor pagination (keyset) for list endpoints; scope-bound HMAC validation. Three per-token and per-pool lists take
?page= - Cache tiers from 1s (HEARTBEAT) to 300s (STATIC)
- Wire types: decimal-dollar strings, Wei strings, lowercased 0x-addresses
- Error envelope, two shapes: handler errors return
{ "error": "message" }(400/403/404/503); schema validation failures return the validator's default{ "success": false, "error": { "name": "ZodError", … } } - ETag revalidation on cached GETs
Build against the routes on this page. Anything else the host answers is operational and can change without notice.