Guides

Claim Rakeback

Fetch a merkle proof and claim your share of protocol fees weekly.

Rakeback is the trader's share of the protocol fee. On every FeeRouter swap, 2 / 7 of the protocol fee is earmarked to the trader. That is the Rakeback bucket's weight in the Collector split, which seeds at 2 : 2 : 2 : 1 across staking, treasury, Rakeback and buyback and burn. Each swap pays 1% at launch: 0.30% to LPs and a 0.70% protocol fee. Rakeback's share of that protocol fee is 0.20% of the swap's quote-side volume. Both the fee and the weights are settable state, so read them live if you model it.

Read the address from /config

/config.addresses.rakeback is the only source for the RakebackV2 address (see the deployment status table). A wallet that paid no protocol fee in an epoch gets an empty list from the /rakeback/* routes rather than an error. So does a wallet that has traded but has no epoch open to it yet: epochs close Sunday 00:00 UTC and open to claim about a day later, so the first root lands the week after the DEX opens.

Payout runs on weekly epochs. The backend derives one merkle tree per epoch from indexed chain data, an automated poster puts the root on chain, and you claim a leaf with its proof. There is no signature to fetch, no deadline on the proof, and no signing key on the claim path.

The lifecycle

1. Trade

You trade via FeeRouter in epoch = floor((block.timestamp - 259200) / 604800) (Unix weeks that start on Sunday. The 259200 offset shifts the Thursday Unix epoch start to Sunday 00:00 UTC).

2. Indexer captures

The indexer captures feeAmount from FeeRouter.Swap and records the accrual in the quote asset, at 2 / 7 of the fee.

3. Backend derives the epoch tree

Once the epoch closes, the backend fans each wallet's raw accruals through the vest (below), sums them per (account, token), and builds one tree for the whole epoch.

4. Poster puts the root on chain

postRoot(epoch, root, tokens[], declaredTotals[]), poster-gated. The declared total per token is capped at the token's measured Collector inflow, so a root can never declare more money than the pot actually received. At most MAX_TOKENS_PER_EPOCH = 8 tokens per epoch.

5. Activation opens claims

activate(epoch, token) is permissionless. It succeeds once the activation delay has elapsed since postRoot and the pot holds enough to cover every active slice for that token plus this one. That funding gate is why an active claim can never fail on funds: claims are delayed, never partial.

The delay is activationDelay, bounded by MIN_ACTIVATION_DELAY and MAX_ACTIVATION_DELAY. Read the live values from the contract. It is the public verification window, and the window in which a guardian can cancelRoot a bad root. Once any slice of an epoch has activated, cancel is closed.

6. Claim within 30 days

CLAIM_WINDOW = 30 days, measured from the slice's Activated timestamp, not from epoch close. After that, expireRoot(epoch, token) releases the unclaimed remainder to the treasury's sweepable surplus.

Fetch claimable state

GET /rakeback/{address} carries everything the claim needs, including the merkle preimage and proof for every published leaf:

const rb = await fetch(
  `https://api.motoswap.org/rakeback/${addr}`,
).then(r => r.json());

// rb.epochs: one row per published (epoch, token) leaf this wallet holds.
// Each row: { epoch, token, status, amount, index, proof, root,
//             activatedAt, claimDeadline, usdValue }
const claimable = rb.epochs.filter(l => l.status === "claimable");

The guide's fetch calls are simple GETs, which work from any origin. A request that triggers a CORS preflight (a script-set If-None-Match, a JSON POST) only passes for the Motoswap app origin, so make those server-side.

status is one of:

StatusMeaning
pendingRoot posted, slice not activated yet. Nothing to submit.
claimableActivated and inside the 30-day window. Submit it.
claimedThe bitmap bit is set on chain.
expiredThe window lapsed, or the root was cancelled or expired.

Two other reads:

GET /rakeback/{address}/epoch/{epoch}   # one epoch's leaves for the wallet
GET /rakeback/{address}/proof           # proofs only, across every published tree
GET /rakeback/stats                     # currentEpoch, nextEpoch, totalEpochs

/rakeback/{address} and /rakeback/{address}/epoch/{epoch} return 503 when the indexer head is lagging. That is deliberate: a stale head would report an expired leaf as still claimable, so the route refuses rather than lying.

/rakeback/{address}/proof returns { items: [{ epoch, token, index, amount, proof, root }] }. An unknown wallet gets an empty items array.

Claim (the normal path)

claimMany is the usual call, because a wallet typically holds several leaves at once:

import { parseAbi } from "viem";

const { epochs } = await fetch(
  `https://api.motoswap.org/rakeback/${addr}`,
).then(r => r.json());

const leaves = epochs.filter(l => l.status === "claimable");

const total = await wallet.writeContract({
  address: rakeback,
  abi: parseAbi([
    "function claimMany(uint256[] epochs, address[] tokens, uint256[] indexes, address[] accounts, uint256[] amounts, bytes32[][] proofs) returns (uint256)",
  ]),
  functionName: "claimMany",
  args: [
    leaves.map(l => BigInt(l.epoch)),
    leaves.map(l => l.token),
    leaves.map(l => BigInt(l.index)),
    leaves.map(() => addr),
    leaves.map(l => BigInt(l.amount)),
    leaves.map(l => l.proof),
  ],
});

All six arrays must be the same length, which mapping over one source list gives you for free. LengthMismatch reverts otherwise.

claimMany is atomic. One bad entry (already claimed, expired, bad proof) reverts the whole batch, so send only entries you believe are claimable.

Pre-filter with on-chain isClaimed

The API list rides indexed events, so a claim submitted from another device (or one the indexer has not caught up to) can still appear as claimable. One already-claimed entry reverts the entire batch. Multicall isClaimed(epoch, token, index) over your candidate set and drop the hits before you build the args. The contract's bitmap cannot be stale.

Claim a single leaf

const paid = 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(epoch), token, BigInt(index), account, BigInt(amount), proof],
});

A leaf is all-or-nothing. The contract flips one bitmap bit and pays the full amount; there is no cumulative counter and no partial claim. Delivery goes to the leaf's account, not to msg.sender, so anyone can push a claim to its rightful owner.

Claim as MOTO (auto-swap in same tx)

const motoOut = await wallet.writeContract({
  address: rakeback,
  abi: parseAbi([
    "function claimAsMoto(uint256 epoch, address token, uint256 index, uint256 amount, bytes32[] proof, uint256 minMotoOut, address[] path, uint256 deadline) returns (uint256)",
  ]),
  functionName: "claimAsMoto",
  args: [BigInt(epoch), token, BigInt(index), BigInt(amount), proof, minMotoOut, [token, motoToken], deadline],
});

There is no account argument: the proof must bind msg.sender as the leaf account, so this one claims your own leaf only. path must start at token and end at MOTO. The swap goes through FeeRouter, so it pays the protocol fee on that leg like any other trade. minMotoOut must be a fee-netted quote (see swap-via-fee-router). The Swap event for that leg names the RakebackV2 contract as user, because it is the caller. When token is already MOTO the swap is skipped and the leaf is transferred straight out.

The 50 / 25 / 25 vest

The vest is applied by the backend at post time and baked into the leaf amount. There is no read-time vest and no cumulative unlock on chain.

Raw accrual R[x] from epoch x is fanned across three epochs by age:

slice0 = (R * 5000) / 10000
slice1 = (R * 2500) / 10000
slice2 = R - slice0 - slice1      // the remainder, so the three sum to R exactly

Epoch x's published pool for a wallet is slice0(R[x]) + slice1(R[x-1]) + slice2(R[x-2]). So the leaf you claim for epoch x already blends this week's half with the tails of the two weeks before it, and you claim it once.

Leaf and tree construction

If you want to verify a proof yourself rather than trust the API:

leaf   = keccak256(abi.encodePacked(uint256 index, address account, address token, uint256 amount))
parent = keccak256(min(a, b) concat max(a, b))

Sorted-pair (commutative) hashing, matching OpenZeppelin's MerkleProof.verify. An odd trailing node is promoted to the next level unhashed. This is not OpenZeppelin StandardMerkleTree, which double-hashes its leaves.

Leaves are ordered by token ascending, then account ascending, and index is the global 0-based position in that ordering. One tree covers the whole epoch across every token; the money, though, is accounted per (epoch, token), so activation, funding, the claim bitmap and expiry are all per slice.

The 30-day expiry

expireRoot(epoch, token) is permissionless and lazy. After activatedAt + CLAIM_WINDOW, it releases the slice's unclaimed remainder from outstanding so the owner can sweep it to the treasury. sweep can only take the surplus above the token's live liability and can only pay the treasury address.

A slice that was posted but never activated exits a different way: cancelRoot(epoch) while pending (guardian or owner), or the owner-only expirePendingSlice(epoch, token) once its activation window has fully lapsed and the pot still cannot fund it. Calling expireRoot on a slice that never activated reverts NotActivated().

Solidity ABI summary

// Reads
function isClaimed(uint256 epoch, address token, uint256 index) view returns (bool);
function epochRoots(uint256 epoch) view returns (bytes32 root, uint64 postedAt);
function slices(uint256 epoch, address token) view returns (uint256 declaredTotal, uint256 claimedTotal, uint64 activatedAt, bool expired);
function outstanding(address token) view returns (uint256);
function activeOutstanding(address token) view returns (uint256);
function totalDeclared(address token) view returns (uint256);
function lifetimeInflow(address token) view returns (uint256);
function accountedBalance(address token) view returns (uint256);
function epochEnd(uint256 epoch) pure returns (uint256);
function activationDelay() view returns (uint64);
function CLAIM_WINDOW() view returns (uint256);
function MIN_ACTIVATION_DELAY() view returns (uint64);
function MAX_ACTIVATION_DELAY() view returns (uint64);
function MAX_TOKENS_PER_EPOCH() view returns (uint256);
function poster() view returns (address);
function guardian() view returns (address);

// Writes (permissionless)
function syncInflow(address token);
function activate(uint256 epoch, address token);
function claim(uint256 epoch, address token, uint256 index, address account, uint256 amount, bytes32[] proof) returns (uint256);
function claimMany(uint256[] epochs, address[] tokens, uint256[] indexes, address[] accounts, uint256[] amounts, bytes32[][] proofs) returns (uint256 total);
function claimAsMoto(uint256 epoch, address token, uint256 index, uint256 amount, bytes32[] proof, uint256 minMotoOut, address[] path, uint256 deadline) returns (uint256 motoOut);
function expireRoot(uint256 epoch, address token);

// Role-gated
function expirePendingSlice(uint256 epoch, address token);                                  // owner
function postRoot(uint256 epoch, bytes32 root, address[] tokens, uint256[] declaredTotals); // poster
function cancelRoot(uint256 epoch);                                                         // guardian or owner
function sweep(address token, uint256 amount);                                              // owner

Events

event RootPosted(uint256 indexed epoch, bytes32 root, uint64 postedAt);
event SlicePosted(uint256 indexed epoch, address indexed token, uint256 declaredTotal);
event Activated(uint256 indexed epoch, address indexed token, uint64 activatedAt);
event Claimed(uint256 indexed epoch, address indexed token, uint256 index, address account, uint256 amount);
event ClaimedAsMoto(address indexed user, address indexed token, uint256 indexed epoch, uint256 amountIn, uint256 motoOut);
event Expired(uint256 indexed epoch, address indexed token, uint256 remainder);
event PendingSliceExpired(uint256 indexed epoch, address indexed token, uint256 declared);
event RootCancelled(uint256 indexed epoch);
event InflowSynced(address indexed token, uint256 delta, uint256 lifetimeInflow);
event Swept(address indexed token, uint256 amount);

Common errors

ErrorCauseFix
RootPending()The slice has not been activated yet.Wait for the activation delay, or call activate yourself.
RootExpired()Past activatedAt + CLAIM_WINDOW.Nothing to do. The remainder went to treasury.
AlreadyClaimed()The bitmap bit for that index is already set.Drop the leaf. Pre-filter with isClaimed.
InvalidProof()Proof does not verify against the epoch root.Refetch the proof. A cancel-and-repost changes the root.
LengthMismatch()claimMany arrays are different lengths.Fix your batch.
BadPath()claimAsMoto path does not run token to MOTO.First element must be token, last must be MOTO.
NoRoot()No root posted for that epoch.Nothing published yet.
NotFunded()activate ran before the pot could cover the slice.Retry after the Collector distributes.
NotYetActivatable()activate ran inside the activation delay.Wait out activationDelay.
Overclaim()The leaf would push the slice past its declaredTotal. A bad root, not a caller error.Nothing the caller can fix. Do not retry that leaf.
NotActivated()expireRoot on a slice that never activated.Nothing to expire.

Things you can skip

  • Nothing off-chain is signed. The only signature involved is the wallet signing its own claim transaction.
  • Proofs do not expire. A published root is immutable, so the API serves proofs from a STATIC cache and you can hold one indefinitely. The one case that invalidates a proof is a guardian cancelRoot followed by a corrected re-post, which is why InvalidProof means refetch.
  • The tokens that pay Rakeback are the quote assets. Enumerate it from GET /rakeback/{address} rather than on chain.

On this page