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 Pointsconst 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.reason | Meaning |
|---|---|
ok | Nothing about this wallet blocks a binding (linkable: true). |
already_linked | The wallet already has a referrer. Permanent. |
already_scored | The 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() };
}| Status | Body | Meaning |
|---|---|---|
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.