moto.fun

Realtime and candles

The Server-Sent Events stream, what it carries and what it does not, and the exact candle rules, including who fills empty buckets.

There is no WebSocket. Realtime is Server-Sent Events on the moto.fun app backend, with polling as the supported fallback, and the Motoswap API's ETags as the cheap way to poll.

The stream

GET <motofunUrl>/backend-fun/<chain-slug>/stream

A firehose. There is no subscription protocol, no query parameter, and no way to filter to one coin or one event type: you receive every event for every coin on that chain and filter locally.

Each message is a named SSE event whose data is the whole payload:

event: trade
data: {"type":"trade","token":"0x…"}

The payload is a notification, not data

Every event carries exactly { type, token }. No amounts, no prices, no block numbers, no trader. The stream tells you what changed, and you re-fetch the REST queries it touched. Any pipeline that tries to reconstruct state from the stream alone will be wrong.

typeFires when
createdTokenCreated was indexed
tradeAny Trade was indexed
frozenFloorReached was indexed
unfrozenA sell past the reopen delay walked a Frozen coin back to Trading
graduatedGraduated was indexed
moto_leg_completed, moto_leg_deferredRetired. Nothing on the current curve emits them

token is always lowercased at publish time.

Connection rules

  • A ping event is written on connect and every 25 seconds. Treat a much longer gap as a dead connection.
  • 2000 concurrent streams globally and 40 per IP by default, both configurable per deployment. Over the global cap you get 503, over the per-IP cap 429, each with a Retry-After header.
  • There is no id: field, no Last-Event-ID handling, and no replay buffer. A reconnect resumes blind with a gap you cannot recover from the stream. On reconnect, re-fetch what you care about, then resume listening. Standard EventSource auto-reconnect works, it just does not backfill.
  • The bus is in-process. One process, one stream.

Polling instead

Nothing requires SSE. The indexer polls every 5 seconds and /tokens is cached for 5 seconds, so polling that service faster than about 5 seconds buys nothing. On the Motoswap API, poll /motofun/* with If-None-Match: those ETags version on the curve's own head block, so a quiet curve returns 304 through every DEX block.

Candles

GET <motofunUrl>/backend-fun/<chain-slug>/tokens/{address}/candles?resolution=<sec>&from=<unix>
ParameterTypeDefaultBounds
resolutioninteger seconds300Clamped to [60, 86400]. A continuous range, not an enum
fromunix secondsnow - 86400Raised to the window floor, then aligned down to a bucket edge

The response is a bare JSON array, not an envelope:

[
  {
    "bucket": 1755993600,
    "low": 1300000000,
    "high": 1400000000,
    "trades": 6,
    "volume": "410000000000000000",
    "open": "1310000000",
    "close": "1395000000"
  }
]

The rules, exactly

  1. Buckets are UTC epoch-aligned: floor(timestamp / resolution) * resolution, no offset.
  2. The window is 2000 buckets, anchored to the coin's last trade, not to wall clock. A dormant coin therefore serves the tail of its own trading life at every resolution instead of an empty set. A from earlier than that floor is raised to it; a from later than it is honoured as given.
  3. One seed row is prepended: the newest bucket strictly before the window, so a client's fill-forward has a bar to bridge from. A response can hold 2001 rows.
  4. Empty buckets produce no row at all. There is no server-side fill-forward, no synthetic OHLC, and no zero-volume bar. The array is sparse.
  5. low and high are JSON numbers. open and close are decimal strings. The asymmetry is real, and open and close are optional: treat them as possibly absent.
  6. volume is an exact decimal wei string and sums buys and sells together. There is no side filter. trades is a count.
  7. A coin with no trades returns [] with status 200. Never a 404, never an error, and there is no nextTime or noData envelope. This is not a TradingView UDF datafeed.

Fill-forward is the client's job

The API is sparse by design and the app fills forward on the client. If you are drawing a chart you must synthesize the flat bars yourself, or your gaps will render as holes rather than as quiet markets.

What the app does, if you want to match it

The app's chart carries 1m, 5m, 10m, 30m, 1h and 6h, mounts at 1 minute, and applies these rules on top of the sparse response:

  • Fill forward to the current bucket. Every empty bucket becomes a flat, zero-volume candle whose open, high, low and close all equal the previous close. Every real bar opens at the previous close. Capped at 10,000 filled buckets.
  • Prices convert from the 1e18 fixed-point values directly. A naive wei-to-float helper with a 1e-6 floor renders a live curve price as zero; curve prices routinely sit near 1e-9 ETH.
  • After graduation the series joins the curve history to the pool history at the graduation bucket, so the chart does not restart. The bucket at the seam keeps its own open rather than inheriting the previous close.

Graduated coins move venue

Once a coin has graduated, its candles come from the DEX side, not the curve. The curve series is frozen history. Use /motofun/coins/{address} to learn the status and the pair addresses, then read the pool chart from the Motoswap API like any other pool.

Where to go next

On this page