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:
| Status | Meaning |
|---|---|
pending | Root posted, slice not activated yet. Nothing to submit. |
claimable | Activated and inside the 30-day window. Submit it. |
claimed | The bitmap bit is set on chain. |
expired | The 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 exactlyEpoch 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); // ownerEvents
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
| Error | Cause | Fix |
|---|---|---|
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
STATICcache and you can hold one indefinitely. The one case that invalidates a proof is a guardiancancelRootfollowed by a corrected re-post, which is whyInvalidProofmeans refetch. - The tokens that pay Rakeback are the quote assets. Enumerate it from
GET /rakeback/{address}rather than on chain.