MMashDiv

How rewards work

The exact fixed-rate reward formula, what earning frequency and auto-compound change, why the accrual engine can never double-pay, how the platform fee is split, the two manual distribution doors, and why none of it exists in on-chain mode.

8 min readUpdated 15 September 2026rewards, apr, accrual, fees, cron, distribution, on-chain

Rewards in the fixed-rate product are computed, not received. The platform runs the APR maths against each position, writes an unclaimed earning row, keeps the admin fee, and books the whole gross amount as money it has committed to pay. There is no external yield feed and nothing reconciles against one.

This page is the arithmetic and the schedule. If you have not read the funding warning on the overview, read it first — it changes how you should read every number below.

There is no formula, no accrual cron, no admin distribution door and no claim in the on-chain product: rewards are observed on the network, they raise the value of every share, and they come out when shares are unstaked. Read In on-chain mode at the foot rather than translating anything below.

The formula

Two methods, chosen platform-wide by stakingDefaultAprCalculationMethod. It is not a per-pool setting.

Simple (the default, and what you get if the setting is unreadable):

reward = amount x (apr / 100) x (daysStaked / 365)

Compound:

reward = amount x ((1 + rate/n)^(n x daysStaked/365) - 1)

n — the compounding periods per year — comes from the position's resolved earning frequency, which is its own snapshot and falls back to the pool's only for a legacy row that predates the column:

Earning frequency n
DAILY 365
WEEKLY 52
MONTHLY 12
END_OF_TERM 1

A zero or negative amount, APR or day count returns zero. daysStaked is measured from the position's startDate, always — never from the last payout.

Worked example. 1,000 USDT at 12% APR for a 30-day lock:

Method Gross reward Admin fee at 20% Net to staker
Simple 9.8630 USDT 1.9726 7.8904
Compound, daily 9.9073 USDT 1.9815 7.9259

The staker's realised rate on the simple pool is 9.6%, not 12%. That is by design — the admin fee comes out of the reward, not out of the advertised APR. The admin overview measures every pool against its net promised APR for exactly this reason, so a healthy pool does not report as underpaying by exactly your fee.

When a reward row is written

The position's earningFrequency sets the accrual interval:

Frequency Interval
DAILY 24 hours
WEEKLY 7 days
MONTHLY 30 days, not a calendar month
END_OF_TERM Never during the term — settled at maturity only

The engine works from the position's startDate in whole intervals: it computes the latest interval boundary that has fully elapsed, caps it at endDate, and settles up to there. A position five hours into a DAILY pool is owed nothing yet; a position 25 hours in is owed one day.

Each position carries a watermark, lastDistributionDate, recording the point the engine has already settled to. END_OF_TERM pools and auto-compound pools never advance it during the term.

Why the engine cannot double-pay

This matters because the cron runs hourly, you can trigger distribution by hand, and settlement runs its own final accrual. Three mechanisms keep those from overlapping:

Accrual is a running total, not an increment. For each period the engine computes the reward owed for the entire elapsed term at the position's snapshotted APR, subtracts everything it has already credited, and writes the difference. Missing a run costs the staker nothing — the next run pays the whole outstanding amount at once.

Every row is keyed to its period. Accrual rows carry a periodBucket of accrual_YYYY-MM-DD, under a unique database index on (positionId, type, periodBucket). A concurrent duplicate collides at the database rather than being credited twice.

One-off payouts live in a different namespace. The "already paid" total only counts rows this engine wrote. An admin bonus is always booked as type BONUS with its own bucket, so it is additive money rather than an advance against the APR curve — otherwise a goodwill payment would silently suppress the staker's real accrual until the curve caught up.

The fee split

Each accrual is split at the moment it is written:

  • The net amount goes to the staker as an unclaimed stakingEarningRecord.
  • The fee — the position's snapshotted adminFeePercentage of the gross — is credited to the Super Admin wallet through the platform fee collector, and only then recorded as a PLATFORM_FEE row on the earnings screen.
  • The gross is recorded as a platform loss: the liability the platform has just taken on.

Recording all three is what makes the profit figure honest. Booking only the fee reported a 20% fee as pure profit while the platform had actually committed to paying the whole accrual — plus 20 against a real net of minus 80.

Two edge cases you can configure yourself into:

At adminFeePercentage of 100 the entire period's gross is routed to the platform, no staker earning row is written, and no notification is sent — the code explicitly avoids telling a user "you earned 0". The position still completes and still returns principal. This is a valid configuration and there is no warning on the form.

The collector credits a real wallet. With no Super Admin role or no user in it, the fee is logged as [CRITICAL] and dropped, and the bookkeeping row is deliberately not written — so the earnings screen never shows revenue that does not exist. When the actor performing the action is the Super Admin, the fee is skipped entirely rather than credited back to themselves.

Auto-compound

autoCompound changes the payout shape completely:

  • No claimable rewards during the term. The claim endpoint returns a 400: "Auto-compound rewards are returned with your principal at maturity and cannot be claimed separately."
  • At maturity the engine computes the whole term's compounded reward in one pass, takes the admin fee, and credits principal plus net reward as a single wallet credit.
  • The position's stored amount is then rewritten to the compounded total for history, but pool capacity is restored by the original principal, never the compounded figure.
  • earningFrequency stops controlling payout timing and instead sets the compounding periods per year.

Auto-compound pools are also rejected by both manual distribution endpoints — there is nothing intra-term to distribute.

The automatic schedule

The hourly cron processStakingPositions — Process Staking Logs on the cron screen — does two jobs, and they obey different rules. It touches fixed-rate positions only; the on-chain product has six tasks of its own.

Maturity settlement always runs. Once a position passes its endDate, the final outstanding reward is accrued, the principal is returned and the position completes — regardless of your distribution settings. Holding someone's principal because an admin toggle is off is a breach of the lock they agreed to, not a configuration choice.

Intra-term accrual is gated by two settings:

Setting Absent Effect
stakingAutomaticEarningsDistribution Treated as on Off means matured positions still settle, but no periodic rewards are credited until you distribute by hand.
stakingEarningsDistributionTime Runs every hour An HH:MM value restricts periodic accrual to runs in that hour, server-local. The minutes are ignored. A blank or malformed value degrades to every hour rather than halting distribution.

Skipping an hour never costs a user anything — accrual is delta-based, so the next qualifying run credits the full outstanding amount.

Operationally the cron processes up to five positions at a time, retries a failed position three times with a five-second gap, and if any position still fails it raises a high-priority in-app notification to a Super Admin listing the position IDs.

Distributing by hand

  1. The APR catch-up is not here - it is API only

There are two distribute endpoints and they do genuinely different things. Both require create.staking.earning.

POST/api/admin/staking/earnings/distributepermission: create.staking.earning
APR accrual catch-up. Credits only the delta beyond what has already been distributed, for active positions in one pool. Supports dryRun.

Use this when automatic distribution has been off, or the cron was down, and you want to bring a pool's positions up to date. It routes every write through the same authority the cron uses, so it can be run repeatedly and cannot collide with the cron. Optional positionIds narrows it to a subset — maximum 100 per call. dryRun: true computes and reports the amounts without writing anything.

POST/api/admin/staking/earning/distributepermission: create.staking.earning
One-off payout of an explicit amount, split pro-rata across active positions. Always booked as BONUS.

Use this to pay a bonus, pass through an unusually good month, or make good on an incident. You give it poolId, a total amount and a distributionType label of regular or bonus. The pool's current admin fee percentage is taken off the top and credited to the platform; the remainder is split across active positions in proportion to their staked amount.

Both labels write BONUS earning rows — the label only stops a "regular" and a "bonus" run in the same cycle from colliding. The whole distribution is atomic: if any share computes to a non-finite number, nothing is written and no fee is booked.

The guard buckets requests by the pool's earning frequency — daily pools get one run per 24-hour window, weekly pools one per 7 days, and END_OF_TERM falls back to daily. A second attempt in the same cycle is refused with the cycle timestamp in the message. The window slides from the epoch rather than flipping at midnight, so two requests a minute either side of midnight fall in the same bucket instead of both slipping through.

Claiming a fixed-rate reward

Rewards sit as unclaimed rows until the user claims them. A claim takes every unclaimed row on that position at once and credits the total to the user's wallet as a STAKING_REWARD transaction, rate-limited to ten claims per hour per user and gated on the withdraw_staking KYC feature.

Three refusals:

  • Auto-compound pools — rewards are not separately claimable.
  • Cancelled positions — their unclaimed rewards were forfeited at cancellation and are never payable.
  • On-chain positions — there is nothing to claim. The door says so in product words before it refuses by mode: "On-chain rewards compound in the pool and are paid when you unstake. To take rewards out, unstake the shares you want to withdraw; there is no separate claim."

A COMPLETED position stays claimable. Rewards earned during the term do not expire when the term ends, and the admin overview counts them as an outstanding liability until they are claimed.

In on-chain mode

Every mechanism above is absent, and the substitutes are not analogues of it.

Fixed-rate On-chain
An APR you type, snapshotted per position No rate at all. apr is null on an on-chain pool, and the projection endpoint answers 503 rather than guessing one
An accrual the cron computes hourly An observation the observer task reads from the network every fifteen minutes
A reward row per period, unclaimed until claimed A rise in the share price, which lifts every holder's position at once
Rewards can only go up A reward can be negative: a validator penalty reduces the pool's value for every holder
A Claim control An Unstake rewards control. The rewards are already in the shares
An admin fee taken off each accrual and credited immediately A commission minted as treasury shares at each observation, which rises and falls with every holder's and is slashed with them. Nothing is revenue until the coins reach the platform's own address
Two manual distribution doors None. There is nothing to distribute; you cannot pay out what the network has not paid

One consequence is worth stating on its own, because it has no fixed-rate equivalent: an exit settles at the lower of the price when it was requested and the price when it settled. A fall between the two is taken by the holder, and a rise between the two is not kept by them — and the same rule applies to the platform's own commission exit, so the treasury is not advantaged by it.

How a stake moves follows the observation and exit path end to end; Operating on-chain staking covers the commission ledger and the slashing reimbursement.

Next: the position lifecycle.