Architecture
How Motoswap's contracts, indexer, and REST API fit together for integrators.
Motoswap has three parts: contracts, an indexer and a REST API. You will use all three, and knowing how they relate explains why API data can trail the chain by a block.
Deployment status is per contract, and Contract addresses owns the table. On
Ethereum (chain 1), MOTO, the launch farming contract (VampChef), UniswapZap,
RewardVestingEscrow, MotoStaking, TreasuryVesting and MotocatStaking went out first. The
DEX contracts (factory, router, FeeRouter, pairs, zap, Collector, RakebackV2, MasterChef,
the creator fee pair, the moto.fun contracts) went out with the DEX. Read every
address from /config: a key that reads 0x0 is unset for the current phase.
The three parts
1. Contracts (on-chain, canonical)
Every write goes to a Solidity contract. Every state change emits an event. There is no off-chain database of writes; the chain is the database. The contracts split into families:
- DEX core -
MotoSwapFactory,MotoSwapPair,MotoSwapRouter02,MotoSwapLibrary. - Fees -
FeeRouter(user front door),Collector(revenue splitter),QuoteAssetRegistry(which tokens are fee-side),RakebackV2(merkle claim),BuybackBurner/BuybackDistributor(buyback + Merkle drop),CreatorFeeRegistry/CreatorFeeVault(per-token creator fees). - Farming -
VampChef(launch farming, ended: Uniswap V2 LP or MOTO → MOTO, one flat window),MasterChef(deployed, not running: no emission campaign, none planned),RewardVestingEscrow(180-day vest, shared by both). - Staking -
MotoStaking(locked MOTO + multi-token revshare),MotocatStaking(NFT escrow). - PvE -
PveVault(escrows PvE tokens and pays them to Motocat stakers). - Token / vesting -
MotoToken,TreasuryVesting. - Peripherals -
MotoSwapZap(single-sided LP on Motoswap),UniswapZap(deployed: zap into a Uniswap V2 LP and stake it in theVampChef),Snapshotter.
Full surface: contracts.
2. Indexer (off-chain, derived)
A QuickNode Streams webhook forwards new blocks (with receipts) to the backend. A per-block handler decodes every event of interest and writes into Postgres in a single transaction. Some tables are then CDC-streamed into RisingWave, which computes derived aggregates (points, leaderboard, pool/token stats, Rakeback per epoch) via materialised views that sink back into Postgres for cheap reads.
You never see the indexer directly. But it means:
- All API data is derived from on-chain events. There is no data on the API that you couldn't reconstruct from the chain yourself. The API just makes it fast and cheap.
- Indexer lag ≈ 1 block, which is about 12 seconds on Ethereum.
GET /chain/headlets you check the indexer's tip vs your own RPC's tip if you need to gate on freshness. - Reorg-safe. Raw events are block-numbered and in the rollback set; aggregates re-derive from them. So a reorg does not leave the API's numbers "stuck".
3. REST API (off-chain, cached)
A Hono backend, base URL
https://api.motoswap.org. Serves reads only (list, chart,
per-user metric, price, config). Writes go to the chain.
- Cached GETs are ETag-versioned. Send
If-None-Matchand expect304./configis the exception: it carriesCache-Controland noETag. Cache tiers:HEARTBEAT(1s, chain head),BLOCK(3s, indexer-fresh, incl. prices),CRON_FAST(15s,/profiles),CRON(30s, analytics/rakeback),STATIC(300s, config). - List endpoints are cursor-paginated with HMAC-scope-bound cursors - a cursor cannot be replayed across queries.
- CORS. Simple GET requests work from a browser on any origin. Requests that need a CORS preflight, such as one where your script sets
If-None-Match, only pass for the Motoswap app origin, so do conditional polling and any write from a server.
Write path - a swap end-to-end
1. User calls FeeRouter.swapExactETHForTokens
2. FeeRouter:
- identifies the "fee side" via QuoteAssetRegistry (WETH here)
- skims 0.70% of the WETH amount → Collector
- calls MotoSwapRouter02.swap*
3. MotoSwapRouter02:
- permissioned via factory.swapAllowed[msg.sender]
- calls MotoSwapPair.swap(...) for each hop
4. MotoSwapPair.swap emits Swap + Sync
5. FeeRouter emits Swap (indexed: user, tokenIn, tokenOut; also feeToken, feeAmount).
`user` is msg.sender, the payer, not the `to` recipient.
--- meanwhile off-chain ---
6. QuickNode Streams forwards the new block + receipts to /webhook.
7. The dispatcher processes the block in a single Postgres tx:
- decodes FeeRouter.Swap → writes points_events (actor + referral), rakeback_accrual_events
- decodes pair Swap → writes activity (display feed) + pool stats
- advances the indexer tip
8. RisingWave CDC-reads the raw tables and re-derives:
- rakeback_epoch(user, token, epoch) aggregate
- leaderboard sink (points sum)
- pool_row / token_row (24h aggregates)
9. Backend routes read from the sinks; ETag = f(chain head + head hash).Your wallet sees the tx receipt long before steps 6 to 8; if you want to know
"is the indexer caught up to my tx", poll GET /chain/head until
blockNumber >= receipt.blockNumber.
Read path - the API is a cache over derived views
The API doesn't compute on the request. It reads a materialised
pool_row / token_row / rakeback_epoch / etc. row and serialises. This
is why:
- A read does no aggregation work. It is one indexed row read plus serialisation.
- ETag hits are near-free. The version fold is a tiny SQL read.
- You should cache clientside - the API tells you exactly when the
cache is safe (
Cache-Control+ETag).
The FeeRouter is the intended front door
Two paths reach a MotoSwapPair.swap:
- FeeRouter → Router02 → Pair - the intended user path. Charges the 0.70% protocol fee.
- Router02 → Pair - bypasses
FeeRouter, butMotoSwapRouter02is perimeter-gated viafactory.swapAllowed[msg.sender]. Only whitelisted callers (of whichFeeRouteris one) can call it. Direct calls revert"MotoSwapRouter: SWAPPER_NOT_ALLOWED"(router) or"MotoSwap: SWAPPER_NOT_ALLOWED"(pair) - note the different prefixes if you string-match reverts.
Add-liquidity and remove-liquidity are permissionless on Router02 -
the perimeter only gates swaps.
The Collector splits fees dynamically
Collector doesn't have a hardcoded split. It has a Bucket[] list of
(recipient, weight, notify). At deploy the split is 2:2:2:1 (staking :
treasury : Rakeback : buyback and burn). The owner can reweight, add, or replace
buckets - a bucket whose payout reverts is "earmarked" and skipped so the
healthy ones aren't reweighted by a broken destination.
Ownership boundaries
MotoToken- fixed supply, no owner, no mint, no pause.- UUPS-upgradeable (with a one-way
renounceUpgradeability):MotoSwapFactory(also renounces beacon separately),MotoSwapPair(via beacon),FeeRouter,RakebackV2,Collector,MasterChef,VampChef,MotoStaking,PveVault,MotocatStaking,BuybackBurner,RewardVestingEscrow,TreasuryVesting. - Not upgradeable (
Ownable2Step):QuoteAssetRegistry,CreatorFeeRegistry,CreatorFeeVault,BuybackDistributor,Snapshotter. - Not upgradeable, no owner:
MotoSwapZap,UniswapZap.
Every UUPS contract exposes renounceUpgradeability() - after that call,
the implementation is frozen. Track it via the Upgraded event on the
proxy or by reading the implementation slot.
Time is Unix seconds, everywhere
Every timestamp on the API is a Unix-seconds integer (not Date, not
timestamptz). This mirrors block.timestamp (chain-native). Format for
display only. The full domain-type cheat sheet lives in
addresses, config and events.
The indexer and database
The indexer and database are internal. You don't run them; the REST API is your interface.