Guides

Points and referrals

Read Points balances and bind referrals. Scoring internals are not published.

Points track activity on Motoswap and count toward the SZN 1 airdrop. The scoring engine runs in the indexer; its internals (rates, curves, tiers, milestone bases, boost formulas) are not published. What is public:

  • Every fee-paying swap earns Points, at any size. Trades on moto.fun earn trading Points too.
  • Motocats multiply swap Points. Staked cats only: a cat earns for the wallet that staked it in MotocatStaking, and only while it is staked. A cat merely held in a wallet counts for nothing.
  • Referrals pay the referrer 10% of the referee's Points, one level, added on top. This includes Points from the referee's moto.fun trades.
  • Balances and the leaderboard publish once a day at the 00:00 UTC drop. Per-swap deltas are never observable via the API.

Before the first daily drop, balances are 0, rank is null and the leaderboard is empty. Every response already has its final shape.

Read Points

GET /points/{address}        # per-wallet balance + rank (updates at the daily drop)
GET /points/leaderboard      # top 100
GET /points/rules            # the canonical public season config
GET /points/weeks            # settled weeks
GET /points/leagues          # current week's leagues
GET /points/fees             # public fee dashboard
GET /points/pair/{pair}      # a pair's public label: "standard" or "burn-verified"
GET /referrals/{address}     # referrer, referee count, referral Points
const wallet = "0x0000000000000000000000000000000000000001";

const p = await (await fetch(`https://api.motoswap.org/points/${wallet}`)).json();
console.log(p.totalPoints, p.rank, p.referral.linkable); // 0 null true

const rules = await (await fetch("https://api.motoswap.org/points/rules")).json();
console.log(rules.referralRateBps); // 1000 = 10%

totalPoints and referralPoints are plain numbers, not token amounts. Render referralPointsDisplay where you show referral Points. An unknown wallet answers 200 with zeros, never 404.

GET /points/rules is the canonical public source. Read season numbers from it at runtime rather than hard-coding them. Its copy block is display text whose wording changes without notice: render it, never parse it.

Bind a referral

A referral binds when the referee signs to confirm it, before their first trade, and cannot be changed after. In API terms: the referee signs a message, you post it, and the API refuses the binding once that wallet has scored Points.

Check first. referral on GET /points/{address} is the same test the write runs:

referral.reasonMeaning
okNothing about this wallet blocks a binding (linkable: true).
already_linkedThe wallet already has a referrer. Permanent.
already_scoredThe wallet has already scored Points. Too late to bind.

Then have the referee sign this exact text with personal_sign and post it:

import type { WalletClient } from "viem";

function referralMessage(referee: string, referrer: string, chainId: number): string {
  return (
    `Activate your Motoswap referral link.\n\n` +
    `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.\n\n` +
    `Referred by: ${referrer.toLowerCase()}\n` +
    `Your wallet: ${referee.toLowerCase()}\n\n` +
    `motoswap-referral:${referee.toLowerCase()}:${referrer.toLowerCase()}:${chainId}`
  );
}

// In the browser: the referee signs. chainId is `chainId` from GET /config.
export async function signReferral(wallet: WalletClient, referrer: `0x${string}`, chainId: number) {
  const referee = wallet.account!.address;
  const signature = await wallet.signMessage({
    account: wallet.account!,
    message: referralMessage(referee, referrer, chainId),
  });
  return { referee, referrer, signature };
}

// On your server: post what the browser sent you.
export async function postReferral(referee: string, referrer: string, signature: string) {
  const r = await fetch("https://api.motoswap.org/referrals/attribute", {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ referee, referrer, signature, source: "my-app" }),
  });
  return { status: r.status, body: await r.json() };
}
StatusBodyMeaning
200{ "bound": true, "binding": { ... } }Bound.
200{ "bound": false, "already": true, "binding": { ... } }The referee already had a referrer. Nothing changed.
400{ "bound": false, "error": "referee-already-active" }The referee has scored Points. Other 400 reasons cover self-referral and cycles.
403{ "bound": false, "error": "signature does not authorize this binding" }The signature is not the referee's, or it was made over different text.

Smart accounts work: when plain signature recovery fails, the API checks ERC-1271 on chain. The signature costs no gas and the binding is off chain. Post from your server: the API's CORS policy is set for the Motoswap app origin, so a browser on another origin blocks this POST. See the API reference for every field.

On this page