Someone is farming the referral programme
What the affiliate engine refuses on its own, what it never will, and the order to investigate a reward spike in — spot it, trace the downline, freeze the condition, then decide the referrals.
The affiliate programme pays real money on conditions you configure. The engine refuses exactly two things by itself — a user referring themselves, and a loop in the tree. Everything else is your judgement: rings of sockpuppet accounts, referrals that deposit the minimum and never trade, and conditions that quietly pay twice for the same event.
This page is what to do when the payout figure jumps.
The screens
All of these are under Extensions → Business Tools → Affiliate Program. The
extension is named mlm internally and its record is titled Multi Level
Marketing, so those words turn up in the extension list and in support
threads — but the menu entry an operator clicks is Affiliate Program.
| Screen | What it is |
|---|---|
/admin/affiliate |
The dashboard — tiles, monthly earnings, the referral status ring, and the ranked list of affiliates by share of total earnings |
/admin/affiliate/referral |
The referral registry. Create, edit, delete. Status lives here |
/admin/affiliate/referral/{id} |
One affiliate in full: their downline network, their reward history, their last six months of earnings |
/admin/affiliate/condition |
The reward conditions — what pays, how much, in what currency, over what period |
/admin/affiliate/reward |
The reward ledger — one row per reward accrued |
/admin/affiliate/settings |
Program settings, including the approval switch |
Permissions: access.affiliate.referral + view.affiliate.referral for the
registry, access.affiliate.reward + view.affiliate.reward for the ledger,
access.affiliate.settings for settings.
/admin/affiliate/condition is not in the route permission map, so opening it
needs only the catch-all access.admin. The API behind it still enforces
view.affiliate.condition and edit.affiliate.condition, so an
under-privileged operator sees an empty editor rather than your commission
rates — but this is one more reason not to hand out access.admin casually.
The three statuses, and which of them pays
A referral row is PENDING, ACTIVE or REJECTED.
Only ACTIVE earns, on both reward paths. The live path
(processDirectRewards and its binary/unilevel siblings) looks up the referral
with status: "ACTIVE" and returns without paying if there is none. The
periodic evaluator loads referrals with status: "ACTIVE" and evaluates nothing
else. So REJECTED — or PENDING — is the lever that stops a referrer earning
another penny, and it takes effect on the next event or the next cron tick with
no restart and no other configuration.
Status is changed one row at a time, from the row's Edit form on
/admin/affiliate/referral (the Settings group carries the status select). The
bulk menu on that table offers Delete and nothing else — there is no bulk
approve or bulk reject in the panel. Editing a row writes an
ADMIN_AFFILIATE row to the audit trail titled Update affiliate referral.
It stops future ones. Rewards that already exist stay in the ledger, claimable, until you deal with them separately — see Dealing with rewards that already exist below.
What the code already refuses
Two guards, both automatic, both worth knowing so you do not go looking for the abuse they already cover.
Self-referral is ignored outright. A user registering with their own
referral code produces no referral row at all — handleReferralRegister returns
early when the referrer and the referred user are the same id, and logs it. The
admin create form enforces the same rule from the other side: creating a
referral where referrer and referred match is refused with
Referrer and referred user cannot be the same.
An ancestor loop is refused. On BINARY and UNILEVEL installs, placing a
referral whose referred user is already an ancestor of the referrer fails with a
409 and the message Referral loop detected: the referred user is already an ancestor. On a binary tree, a referrer whose two child slots are full has the
new member placed by breadth-first search into the next free slot below; when
the whole subtree is full, the placement is refused with No available position in the binary tree.
What it does not refuse
None of these are detected, blocked or even flagged. They are yours to find.
- Many accounts from one IP. Nothing correlates registrations by address. There is no shared-IP report anywhere in the panel.
- Accounts that deposit the minimum and never trade. A condition with a
minAmountof 50 pays on a 50-unit deposit. Ten accounts depositing 50 each and withdrawing it again is a working attack against any deposit-based condition. - A binary position bought by shuffling accounts. Placement is by referral order and breadth-first spillover; there is no cost and no lock-in to sitting in a good position.
- One person holding both sides of a two-sided condition.
NFT_PURCHASE/NFT_SALEandP2P_TRADE/P2P_TRADE_COMPLETIONdeliberately pay both sides of one trade, because normally those are two different people.
Turn the programme from automatic to reviewed
This is the single control that changes the shape of the problem.
Admin → Extensions → Business Tools → Affiliate Program → Settings → General →
Program Settings → Require Approval (affiliateRequireApproval).
With it on, a new referral is created PENDING and earns nothing until you
edit it to ACTIVE. On BINARY and UNILEVEL installs it is also held out of
the tree while it waits, so a pending — or later rejected — referral never
occupies a position, and in binary never eats one of only two child slots.
With it off, referrals are created ACTIVE at registration and start
earning immediately.
The engine reads affiliateRequireApproval — the key the settings screen writes
— and falls back to the older referralApprovalRequired only when the first has
never been set. Neither key is seeded, so on an install where nobody has opened
that screen, approval is off and every referral is ACTIVE from the moment it
is created. If you intend to review referrals, open the screen and set the
switch explicitly rather than assuming the default.
Approving a held referral through the row's Edit form sets the status and
nothing else — the form rebuilds tree nodes only when you change which users
the referral is between. The tree placement that normally happens at
registration was skipped while the referral was PENDING.
So on a BINARY or UNILEVEL install running with approval on, open
/admin/affiliate/referral/{id} after approving and confirm the member actually
appears in the downline. DIRECT installs are unaffected — they have no tree,
and the direct and cron reward paths read the referral row itself.
Conditions: where a programme accidentally pays twice
/admin/affiliate/condition is a sidebar of conditions and an editor. Each one
has a type, a reward, a reward type (FIXED or PERCENTAGE), a reward
currency, a minimum amount and a period (DAILY, WEEKLY, MONTHLY).
Two ACTIVE conditions overlap when they share a type or when their types
resolve to a common ledger transaction type. The second test is the one that
catches the case a group-by-type report cannot see:
| Condition type | Ledger transaction type it is evaluated against |
|---|---|
TRADE |
EXCHANGE_ORDER |
SPOT_TRADE |
EXCHANGE_ORDER |
COPY_TRADING |
EXCHANGE_ORDER |
TOKEN_PURCHASE |
EXCHANGE_ORDER |
Four differently-named conditions, one spot fill. Enable all four at 5% and a referrer earns 20% of that order.
The platform refuses to let that happen silently. Enabling a condition that
competes with one already ACTIVE comes back as a 409 naming the competitors
and the combined rate — for example "Trade Commission also pays on
EXCHANGE_ORDER; Spot Trade Bonus is already active there. Enabling this as well
makes the combined rate 8%." The editor turns that into a confirmation rather
than an error, and re-sends the request with the acknowledgement if you say yes.
Disabling is never guarded, because turning payouts off cannot cost anyone
money.
Three pairs are exempt because they are two-sided by design and are not double
payment: NFT_PURCHASE / NFT_SALE, P2P_TRADE / P2P_TRADE_COMPLETION, and
FX_TRADE_COMMISSION / FX_TRADE_VOLUME.
Two engines pay these, and only one is a cron
This matters when you want to stop the money.
- Event-driven conditions are paid the moment the transaction happens, by
the code path that created it.
WELCOME_BONUS,ECOMMERCE_PURCHASE,ICO_CONTRIBUTION,STAKING,AI_INVESTMENT,INVESTMENT,NFT_PURCHASE,NFT_SALE,P2P_TRADE,COPY_TRADING,FUTURES_TRADE,BINARY_WIN,FX_TRADE_COMMISSIONand their siblings are on that list. - Everything else is paid by
processMlmReferralConditions, which runs hourly and appears on/admin/system/cronas Process MLM Referral Conditions.
The evaluator loads conditions with status: true and skips everything else, so
setting a condition inactive stops it — but that is the only thing that stops
it. Three reasons waiting for the cron not to run is not a plan:
- Event-driven conditions do not touch the cron at all. They pay on the next transaction, whatever the scheduler is doing.
- The evaluator re-checks the previous calendar period as well as the current one on every run, so a qualifying transaction is not lost because the scheduler was down when the period ended.
- Rewards are idempotent per condition, referrer, referred user and calendar period, so a run you missed is simply performed later.
Dealing with rewards that already exist
/admin/affiliate/reward is the ledger. A row carries the referrer, the
condition that granted it, the amount, and one lifecycle flag: Claimed.
isClaimed is the only flag on a reward. There is no approved / paid / failed
transition and no payout row behind it. The money actually moves when the
referrer presses Claim on their own affiliate page — that is what credits
their wallet, and the claim endpoint only accepts a reward that is still
unclaimed and belongs to the caller.
That gives you two levers on an unearned reward, and they trade off against each other:
- Edit the row and tick Claimed. The claim endpoint will never match it again, so it can never pay — but the ledger now reads as though it was paid, which is misleading to whoever reads it next.
- Delete the row (single or bulk, needs
delete.affiliate.reward). The reward is gone and the ledger is honest about the total, but the evidence of what the engine calculated goes with it. Either path lands on the audit trail, but under two different titles: deleting one row is Delete affiliate reward, and the table's bulk delete is Bulk delete affiliate rewards. Who did it and when survives either way — just filter on the right one, and remember that clearing a ring's rewards from a filtered ledger is the bulk title.
Prefer delete when you are confident, and do it before the referrer claims — once claimed, the wallet has been credited and you are into a balance adjustment rather than a ledger edit. See Customer wallets and balance adjustments.
The investigation, in order
-
Spot it on
/admin/affiliate. The tile row gives total earnings and earning affiliates; the monthly earnings chart gives the shape. The thing to look at is the ranked affiliate list at the bottom — its bar is share of total programme earnings, which is bounded by construction and answers the only question that matters at this stage: is one account taking most of the payout.Two of the summary lines have exact meanings worth knowing before you read anything into them. Awaiting approval counts referral rows still
PENDING. Awaiting payout means "reward accrued, not yet marked claimed", because there is no approval state to count instead. And when your conditions do not all pay in the same currency, the total prints without a symbol and the page says mixed — it is a cross-currency sum and it is not money. -
Open the referrer. Expand their row on
/admin/affiliate/referraland press View details, or go to/admin/affiliate/referral/{id}directly. That page renders their downline network, their reward history and their last six months of earnings. A ring shows up here as a level that is suspiciously wide and was filled suspiciously fast — twenty members who all registered inside an hour is not organic growth. -
Cross-check the referred accounts. Take the referred users from the network view into
/admin/crm/user. There is no shared-IP report, so this is done one account at a time: open the account and read the Activity tab, which lists recent sign-ins with the IP address and User-Agent recorded for each. Same address and same device string across a level is the finding. Registration dates clustered in a few minutes, and identical or near-identical email patterns, are the corroboration. -
Freeze the money before you touch the accounts. Set the paying condition inactive on
/admin/affiliate/condition. This is step four and not step one only because you want to know which condition is paying first — but do it before you start editing referrals, because until it is off, every event that arrives is still paying. -
Decide the referrals. Edit each referral row and set it
REJECTED(orPENDING, if you want to hold rather than refuse). Both stop future earning on both paths. -
Deal with the accrued rewards.
/admin/affiliate/reward, filtered to that referrer. Delete the unclaimed rewards that came from the ring, or mark them claimed to freeze them — see the trade-off above. -
Turn approval on. If it was off,
/admin/affiliate/settings→ Require Approval. It will not undo anything, but the next ring landsPENDINGand earns nothing while you look at it. -
Check the condition before you re-enable it. If the ring exploited a
minAmountthat was too low, or an overlap that paid four times on one fill, fix the condition rather than re-enabling it as it was.
Everything here lands on the audit trail
Every mutation on these screens declares module ADMIN_AFFILIATE and is
recorded at /admin/system/audit: Update affiliate referral, Create
affiliate referral, Delete affiliate reward, Bulk delete affiliate
rewards, Update affiliate condition, Update affiliate condition
status. Titles are per-endpoint, so a bulk action never carries the single-row
title. Filter Module to ADMIN_AFFILIATE — rather than filtering by title — to
read back exactly what you did during an investigation, which is the record you
will want if the affiliate disputes it.
Related
- The admin audit trail — reading those rows back.
- The scheduler console — where Process MLM Referral Conditions lives, and how to tell whether it ran.
- The user desk — the Activity tab, and blocking an account.
- Customer wallets and balance adjustments — what to do once a reward has been claimed and the wallet is already credited.