Quickstart

Read the deployed addresses, one price and one on-chain value, then run the DEX examples end to end.

Deployment status is per contract. On Ethereum (chain 1):

StatusContractsHow you tell
Deployed firstMOTO, the launch farming contract (a 14-day farm: Uniswap V2 LP tokens or MOTO in, MOTO out), UniswapZap, MotoStaking, RewardVestingEscrow, MotocatStakingThe /config address is non-zero
Deployed with the DEXThe DEX contracts: factory, router, FeeRouter, pairs, zap, RakebackV2, the creator fee pair, and the moto.fun contractsThe /config address is non-zero and /config.launchPhase reads dex

The table with every address is on Contract addresses.

Every section below runs as written, with one wait built in: Rakeback pays on weekly epochs, so section 8 answers empty until the first root is posted. A /config key still reads 0x0000000000000000000000000000000000000000 when its contract is unset for the current phase, so every example that sends a transaction checks for that first.

You need:

  • Node 18 or later (for the built-in fetch) and viem. The examples are plain ES modules: save one as example.mjs and run node example.mjs.
  • An RPC endpoint for chain 1. You supply your own: the examples read it from the RPC_URL environment variable and stop with a clear error when it is unset. Never ship a keyed RPC URL in a browser bundle.
  • A funded wallet, only for the examples that send a transaction.

You do not need to run any Motoswap software locally.

API host: https://api.motoswap.org is the permanent public API host and the only one to build against. 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.


1. Discover addresses

Ask the backend, and take the chain id from the same response instead of assuming one:

const API = "https://api.motoswap.org";
const ZERO = "0x0000000000000000000000000000000000000000";

const cfg = await fetch(`${API}/config`).then((r) => r.json());

console.log(cfg.chainId);      // 1
console.log(cfg.launchPhase);  // "dex"

const { motoToken, vampChef, feeRouter } = cfg.addresses;
console.log("MOTO:", motoToken); // 0xbd965230588eaa536de6aa45e8ebbc01638535e0
console.log("farm:", vampChef);  // 0x51e648f08a9a08a724591938d9cbc483c809aca4
console.log("FeeRouter deployed:", feeRouter !== ZERO); // true

Addresses come back lowercase. A key that reads 0x0 means that contract is unset for the current launchPhase. Never send a transaction to an address you have not compared against ZERO. The full list of addresses is on Contract addresses.

The response is Cache-Control: public, max-age=300, s-maxage=300, stale-while-revalidate=900 with no ETag, so cache it locally and refresh every 5 minutes.


2. Fetch a price

Option A: REST

const API = "https://api.motoswap.org";

const cfg = await fetch(`${API}/config`).then((r) => r.json());
const moto = cfg.addresses.motoToken;

const price = await fetch(`${API}/token/${moto}/price`).then((r) => r.json());
console.log(price.priceUsd); // priceUsd: "<decimal string>", or null when unknown
console.log(price.source);   // where the price came from, e.g. "onchain"

Every token the indexer tracks, with priceUsd and ethPrice, comes back from GET /holdings/tokens in one call.

GET /tokens/{address}/chart?range=24h serves candles and the 24h volume window. Accepted ranges: 1m, 5m, 10m, 30m, 24h, 7d, 30d. Its price is nullable and candles can be empty for a token with no Motoswap trading history, so fall back to /token/{address}/price when you get a null.

Send If-None-Match with the previous ETag on subsequent calls and you get 304 almost always. See guides/caching-and-etags.

Option B: chain-direct (fresh reserves)

Read getReserves() on a pair and price it yourself. The MOTO/WETH pair for the current phase is served as motoUniswapPair. Motoswap pairs expose the same getReserves() shape, so the code below works against either.

import { createPublicClient, defineChain, formatUnits, http, parseAbi } from "viem";

const API = "https://api.motoswap.org";
const RPC_URL = process.env.RPC_URL;
if (!RPC_URL) throw new Error("Set RPC_URL to your own Ethereum mainnet RPC endpoint.");

const cfg = await fetch(`${API}/config`).then((r) => r.json());
const { motoToken, motoUniswapPair } = cfg.addresses;

const chain = defineChain({
  id: cfg.chainId,
  name: "Motoswap target chain",
  nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
  rpcUrls: { default: { http: [RPC_URL] } },
});
const client = createPublicClient({ chain, transport: http(RPC_URL) });

const pairAbi = parseAbi([
  "function token0() view returns (address)",
  "function getReserves() view returns (uint112, uint112, uint32)",
]);
const token0 = await client.readContract({ address: motoUniswapPair, abi: pairAbi, functionName: "token0" });
const [r0, r1] = await client.readContract({ address: motoUniswapPair, abi: pairAbi, functionName: "getReserves" });

// Both tokens have 18 decimals, so no decimal rescale is needed here.
const motoIsToken0 = token0.toLowerCase() === motoToken;
const [motoReserve, wethReserve] = motoIsToken0 ? [r0, r1] : [r1, r0];
const ethPerMoto = (wethReserve * 10n ** 18n) / motoReserve; // 18-decimal fixed point, bigint
console.log(formatUnits(ethPerMoto, 18)); // "<decimal string>", ETH per MOTO

Option C: candidate pairs for a route

const API = "https://api.motoswap.org";

const cfg = await fetch(`${API}/config`).then((r) => r.json());
const { motoToken, weth } = cfg.addresses;

const { pairs } = await fetch(
  `${API}/swap/candidates?tokenIn=${motoToken}&tokenOut=${weth}`,
).then((r) => r.json());

console.log(pairs); // [{ address, token0, token1 }, ...]

The response carries pair addresses and their tokens, not reserves. Fetch live reserves yourself (multicall), then price the paths. See guides/read-prices for the read-path tradeoffs.


3. Read the chain

One MOTO read and one farm read, straight from an RPC:

import { createPublicClient, defineChain, http, parseAbi } from "viem";

const API = "https://api.motoswap.org";
const RPC_URL = process.env.RPC_URL;
if (!RPC_URL) throw new Error("Set RPC_URL to your own Ethereum mainnet RPC endpoint.");

const cfg = await fetch(`${API}/config`).then((r) => r.json());
const { motoToken, vampChef } = cfg.addresses;

const chain = defineChain({
  id: cfg.chainId,
  name: "Motoswap target chain",
  nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
  rpcUrls: { default: { http: [RPC_URL] } },
});
const client = createPublicClient({ chain, transport: http(RPC_URL) });

const erc20 = parseAbi([
  "function symbol() view returns (string)",
  "function decimals() view returns (uint8)",
  "function totalSupply() view returns (uint256)",
]);
const chef = parseAbi([
  "function poolLength() view returns (uint256)",
  "function poolInfo(uint256 pid) view returns (address lpToken, uint256 allocPoint, uint256 lastRewardTime, uint256 accRewardPerShare, uint256 lpSupply)",
  "function emissionEnd() view returns (uint64)",
]);

const [symbol, decimals, supply] = await Promise.all([
  client.readContract({ address: motoToken, abi: erc20, functionName: "symbol" }),
  client.readContract({ address: motoToken, abi: erc20, functionName: "decimals" }),
  client.readContract({ address: motoToken, abi: erc20, functionName: "totalSupply" }),
]);
console.log(symbol, decimals); // MOTO 18
console.log(supply);           // totalSupply: <bigint>

const pools = await client.readContract({ address: vampChef, abi: chef, functionName: "poolLength" });
const [lpToken, allocPoint, , , lpSupply] = await client.readContract({
  address: vampChef, abi: chef, functionName: "poolInfo", args: [0n],
});
const end = await client.readContract({ address: vampChef, abi: chef, functionName: "emissionEnd" });

console.log(pools);                    // bigint pool count
console.log(lpToken, allocPoint);      // pool 0 stakes the MOTO/WETH Uniswap V2 LP token
console.log(lpSupply);                 // bigint, LP tokens staked in pool 0
console.log(new Date(Number(end) * 1000).toISOString()); // when farm emissions stop

All token amounts are bigint. Never convert a wei amount to a JS Number.


4. Stake LP into a farm

The deployed farm is the launch farming contract, a VampChef. Its pools take Uniswap V2 LP tokens. Every row of GET /farms names the chef that owns the pool, so deposit to pool.chef instead of hard-wiring a /config key. While launchPhase was vamp, /config.addresses.masterChef held the VampChef too, which has a one-step withdraw and its own event set.

import { createPublicClient, createWalletClient, defineChain, erc20Abi, http, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";

const API = "https://api.motoswap.org";
const RPC_URL = process.env.RPC_URL;
if (!RPC_URL) throw new Error("Set RPC_URL to your own Ethereum mainnet RPC endpoint.");

const cfg = await fetch(`${API}/config`).then((r) => r.json());
const chain = defineChain({
  id: cfg.chainId,
  name: "Motoswap target chain",
  nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
  rpcUrls: { default: { http: [RPC_URL] } },
});
const PRIVATE_KEY = process.env.PRIVATE_KEY;
if (!PRIVATE_KEY) throw new Error("Set PRIVATE_KEY to a funded test wallet key.");
const account = privateKeyToAccount(PRIVATE_KEY);
const client = createPublicClient({ chain, transport: http(RPC_URL) });
const wallet = createWalletClient({ chain, account, transport: http(RPC_URL) });

// 1. Find the pool for the LP token you hold. This one is MOTO/WETH.
const myLp = cfg.addresses.motoUniswapPair;
const farms = await fetch(`${API}/farms`).then((r) => r.json());
const pool = farms.pools.find((p) => p.lpToken === myLp.toLowerCase());
if (!pool) throw new Error("no farm for that LP token");

// 2. Stake your whole LP balance: approve the chef, then deposit.
const amount = await client.readContract({
  address: myLp, abi: erc20Abi, functionName: "balanceOf", args: [account.address],
});
if (amount === 0n) throw new Error("this wallet holds no LP tokens");

const approveHash = await wallet.writeContract({
  address: myLp, abi: erc20Abi, functionName: "approve", args: [pool.chef, amount],
});
await client.waitForTransactionReceipt({ hash: approveHash });

const depositHash = await wallet.writeContract({
  address: pool.chef,
  abi: parseAbi(["function deposit(uint256 pid, uint256 amount)"]),
  functionName: "deposit",
  args: [BigInt(pool.pid), amount],
});
console.log(depositHash);

On the VampChef, withdraw(pid, amount) settles rewards and returns your LP in the same call. harvest(pid) pays pending MOTO: half liquid, half into RewardVestingEscrow over 180 days.

MasterChef is deployed but not running: no emission campaign is running and none is planned. LP already staked in it can be withdrawn at any time. emergencyWithdraw(pid) returns it in one call. The regular withdraw is two steps: queue it with withdraw(pid, amount), then claim with claimWithdrawal(pid). With no campaign running there is no cooldown between them. VampChef has no claimWithdrawal.

Full recipe: guides/stake-lp-into-farm.


5. Read a user's state

Every per-user view has a REST endpoint that 304s on the wallet's activity watermark, so polling is free once it is cached:

const API = "https://api.motoswap.org";
const addr = "0x0000000000000000000000000000000000000001"; // any wallet, lowercase or checksummed

// Activity watermark (note: no /user prefix, the path is /{address}/meta)
const meta = await fetch(`${API}/${addr}/meta`).then((r) => r.json());
console.log(meta.lastActivityBlock);

// Points and rank, as of the last daily drop
const points = await fetch(`${API}/points/${addr}`).then((r) => r.json());
console.log(points.totalPoints, points.rank);

// Rakeback per token and epoch. One row per published leaf, with its merkle proof.
const rb = await fetch(`${API}/rakeback/${addr}`).then((r) => r.json());
console.log(rb.currentEpoch, rb.epochs.length);

Portfolio, holdings, farm positions, staking positions and referrals are one call each. See api.


6. Make your first swap

Swap ETH for MOTO through FeeRouter. The example refuses to send if /config hands back a zero address or a phase other than dex.

import { createPublicClient, createWalletClient, defineChain, http, parseAbi, parseEther } from "viem";
import { privateKeyToAccount } from "viem/accounts";

const API = "https://api.motoswap.org";
const ZERO = "0x0000000000000000000000000000000000000000";
const RPC_URL = process.env.RPC_URL;
if (!RPC_URL) throw new Error("Set RPC_URL to your own Ethereum mainnet RPC endpoint.");

// 1. Fetch addresses and refuse to continue unless the DEX surface is there.
const cfg = await fetch(`${API}/config`).then((r) => r.json());
const { feeRouter, router, weth, motoToken } = cfg.addresses;
if (cfg.launchPhase !== "dex" || feeRouter === ZERO || router === ZERO) {
  throw new Error("The DEX surface is not available: /config reads 0x0 or a phase other than dex.");
}

const chain = defineChain({
  id: cfg.chainId,
  name: "Motoswap target chain",
  nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
  rpcUrls: { default: { http: [RPC_URL] } },
});
const PRIVATE_KEY = process.env.PRIVATE_KEY;
if (!PRIVATE_KEY) throw new Error("Set PRIVATE_KEY to a funded test wallet key.");
const account = privateKeyToAccount(PRIVATE_KEY);
const client = createPublicClient({ chain, transport: http(RPC_URL) });
const wallet = createWalletClient({ chain, account, transport: http(RPC_URL) });

// 2. Quote it (chain-direct, single hop). `getAmountsOut` lives on the raw
// router. FeeRouter skims `protocolFeeBps` from the path's quote leg. For
// WETH -> MOTO that is the INPUT side (WETH outranks MOTO), so quote on the
// net input. The side moves per pair; the general rule and the output-side
// case are in the aggregator guide (or ask feeRouter.feeOnOutput(path)).
const path = [weth, motoToken];
const amountIn = parseEther("0.01");
const feeBps = await client.readContract({
  address: feeRouter,
  abi: parseAbi(["function protocolFeeBps() view returns (uint256)"]),
  functionName: "protocolFeeBps",
});
const netIn = amountIn - (amountIn * feeBps) / 10_000n;
const amounts = await client.readContract({
  address: router,
  abi: parseAbi(["function getAmountsOut(uint256 amountIn, address[] path) view returns (uint256[])"]),
  functionName: "getAmountsOut",
  args: [netIn, path],
});
const quotedOut = amounts[amounts.length - 1];

// 3. Apply slippage tolerance: 50 bps = 0.5%.
const minOut = (quotedOut * (10_000n - 50n)) / 10_000n;

// 4. Submit.
const hash = await wallet.writeContract({
  address: feeRouter,
  abi: parseAbi([
    "function swapExactETHForTokens(uint256 amountOutMin, address[] path, address to, uint256 deadline) payable returns (uint256[])",
  ]),
  functionName: "swapExactETHForTokens",
  args: [minOut, path, account.address, BigInt(Math.floor(Date.now() / 1000) + 600)],
  value: amountIn,
});
console.log(hash);

You get one FeeRouter.Swap event with (user, tokenIn, tokenOut, amountIn, amountOut, feeToken, feeAmount) and one MotoSwapPair.Swap per hop. user is msg.sender, the payer, not the to recipient.

More on FeeRouter and the perimeter it fronts: contracts#fees.


7. Add liquidity

For token-token, call MotoSwapRouter02.addLiquidity directly (permissionless). router is cfg.addresses.router; check it against ZERO first, as in section 6. wallet and account are the clients from section 6, and the amounts are bigint.

const deadline = BigInt(Math.floor(Date.now() / 1000) + 600);

const addHash = await wallet.writeContract({
  address: router,
  abi: parseAbi([
    "function addLiquidity(address tokenA, address tokenB, uint256 amountADesired, uint256 amountBDesired, uint256 amountAMin, uint256 amountBMin, address to, uint256 deadline) returns (uint256, uint256, uint256)",
  ]),
  functionName: "addLiquidity",
  args: [tokenA, tokenB, amountA, amountB, minA, minB, account.address, deadline],
});

For ETH-token, addLiquidityETH (payable). For single-sided, use MotoSwapZap.zapInToken / zapInETH (cfg.addresses.zap, checked against ZERO the same way).

Full recipes: guides/add-remove-liquidity (router paths) and contracts#peripherals (Zap).


8. Claim Rakeback

Rakeback accrues from the protocol fee, and the protocol fee is charged by FeeRouter. A wallet that has paid no protocol fee in an epoch gets an empty epochs array from GET /rakeback/{address}. Epochs close Sunday 00:00 UTC and open to claim about a day later, so a wallet that has traded gets an empty array too until its first epoch opens.

Claims are merkle proofs against a weekly root posted on chain. The wallet's proofs come back with its accrual, so one fetch is enough. wallet and account are the clients from section 6.

// rb.epochs: one row per published leaf, carrying its own merkle preimage:
// { epoch, token, status, amount, index, proof, root, ... }
const rakeback = cfg.addresses.rakeback;
if (rakeback === ZERO) throw new Error("/config serves no rakeback address for this phase.");

const rb = await fetch(`${API}/rakeback/${account.address}`).then((r) => r.json());

const l = rb.epochs.find((x) => x.status === "claimable");
if (l) {
  const claimHash = await wallet.writeContract({
    address: rakeback,
    abi: parseAbi([
      "function claim(uint256 epoch, address token, uint256 index, address account, uint256 amount, bytes32[] proof) returns (uint256)",
    ]),
    functionName: "claim",
    args: [BigInt(l.epoch), l.token, BigInt(l.index), account.address, BigInt(l.amount), l.proof],
  });
  console.log(claimHash);
}

Batch with claimMany(epochs[], tokens[], indexes[], accounts[], amounts[], proofs[]). It is atomic, so send only leaves that are still claimable.

Recipe: guides/claim-rakeback.


Next steps

On this page