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.
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
adminFeePercentageof the gross — is credited to the Super Admin wallet through the platform fee collector, and only then recorded as aPLATFORM_FEErow 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
amountis then rewritten to the compounded total for history, but pool capacity is restored by the original principal, never the compounded figure. earningFrequencystops 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
- 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.
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.
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.