Investigating one affiliate
The referral detail screen behind a row — what its profile card, network and reward panels really read, which figures on it are placeholders, and what to use instead.
Running the programme covers the Referrals list. This page covers the screen behind a row — the one you actually open when a member disputes their downline, or when a cluster of referrals looks like a ring.
It is worth reading before you use it, because several of its figures are not what their labels say, and one panel is empty of everything except row count.
Reaching it
| Route | /admin/affiliate/referral/{id} |
| Page permission | access.affiliate.referral |
| Data endpoint | GET /api/admin/affiliate/referral/{id}, permission view.affiliate.referral |
There is no menu entry. From Admin → Affiliate → Referrals, expand a row and press View Details; the same screen opens as a right-hand sheet titled Affiliate Referral Details. The standalone URL renders the identical component full-page with a back arrow to the list — it is the one to use when you want to send somebody a link.
The {id} in the URL is the referral row's id, not a user id.
Opening a row loads that row's referrer and then everything about them: all of their referrals, all of their rewards, their whole six-month earnings history. Two different rows belonging to the same referrer open the same screen.
So this is not "look at this one relationship" — it is "look at this affiliate's entire book, reached through one of their referrals". That is what makes it the right screen for a downline dispute and the wrong one for checking a single pairing (use the list's view dialog for that; it shows the referrer → referred pair and the record's status).
The same referral data also appears as an affiliate tab on the CRM user detail
screen, /admin/crm/user/{id}, if you are already looking at a member there.
The profile card
Always visible on the left, in both the sheet and the full page.
| Field | What it actually is |
|---|---|
| Name, Affiliate ID | The referrer user's name and user id |
| Status badge | The referrer's account status — active, inactive, suspended, banned — not the referral's PENDING/ACTIVE/REJECTED |
| Email, Phone | From the user record; missing values read "Not provided" |
| Location | Always "Not provided" — the endpoint hard-codes it to null |
| Joined | The referral row's creation date, not the user's registration date |
| Referral Link | See the warning below |
The block under Referral Link renders https://example.com/ref/ followed by
the referral row's id. Both halves are wrong: the origin is a literal in the
component, and the code is a referral record id rather than a user id.
The real member link is their own user id on the register route —
https://your-site.com/en/register?ref=<user id>. Never copy this box to a
member. What members see has the real format
and the generator that produces it.
Three stat tiles sit beside the card: Total Referrals (count of referral rows where this user is the referrer), Total Earnings, and Conversion Rate.
It is SUM(reward) across every reward this affiliate has ever earned,
regardless of which condition — and therefore which rewardCurrency — produced
each one, printed with a $. If your conditions pay in more than one currency,
this number is not money in any unit.
The converted, honest version of this figure is on Rewards → Analytics; see The referral and reward analytics headers. The same applies to the Earnings column in the Network table below.
Conversion Rate here is reward rows ÷ referral rows, as a whole percentage. It is not a conversion of anything in the marketing sense, and it can exceed 100% — an affiliate with 2 referrals and 6 rewards reads 300%.
The three tabs
Performance opens by default.
Performance
Earnings History — a line chart of this affiliate's reward totals per month
for the last six months, from SUM(reward) grouped by month. Real data, same
cross-currency caveat as above.
Performance Metrics — three progress bars. Read this next part before you quote any of them.
The detail endpoint returns no click-through rate and no average order value. There is no click tracking in this product and no order value on a referral. The component falls back to fixed constants, so every affiliate on your platform shows Click-through Rate 8.3% and Average Order Value $125.
Worse, the Conversion Rate bar uses the same fallback: an affiliate whose real conversion is 0 displays 12.5%, because zero is treated as "no value" and replaced by the placeholder. A genuinely non-zero rate is shown correctly.
Do not use this panel for anything. The affiliate's real numbers are the two tiles above it and the analytics panels on the list screens.
Network
A table of this affiliate's downline: name, email, level, status, referral count, earnings and join date. This panel is real, and how deep it goes depends on the structure set on Programme settings:
| Structure | What the table contains |
|---|---|
DIRECT |
The affiliate's immediate referrals only, all at level 1 |
BINARY |
A breadth-first walk of mlm_binary_node from this referral's node, following leftChildId / rightChildId, with the depth as the level |
UNILEVEL |
A breadth-first walk of mlm_unilevel_node from this referral's node by parentId |
Each row's Status is again the user's account status, not the referral's. Referrals and Earnings are that person's own totals as a referrer, so a downline member with a large number there is running their own sub-programme.
Under BINARY, placement, the two-child limit and spillover decide this
picture and nothing else. Commission is paid by walking the sponsor chain —
who invited whom — in mlm_referral.referrerId, for both BINARY and
UNILEVEL alike.
So a member who appears three levels down in this table because of spillover is still paid as a level-1 referral of whoever actually invited them, and moving a node would change the diagram without changing one payout. Referral structures explains why.
Rewards
A table of this affiliate's rewards, newest first: Date, Type, Description, Status, Amount.
The endpoint returns each reward as date / type / description / status /
amount, and the table reads createdAt / conditionId / referrerId /
isClaimed / reward. None of those names match, so on a live install every
row renders as -, -, -, a Pending badge and $0, however many real
rewards the affiliate has and whatever their state.
The number of rows is correct. Nothing in them is. Use
/admin/affiliate/reward, filtered by referrer, for the real ledger.
The tree-node endpoints
Two admin endpoints return the tree as a nested structure rather than a flat
table. Both carry view.affiliate.referral.
Three things to know about them:
- The
{id}is a USER id, not a referral id — unlike the detail route above it. Passing a referral id returns a 404 "User not found". - They are not what the Network tab is drawing. No admin screen calls either
one; the detail screen builds its table from
GET /api/admin/affiliate/referral/{id}alone. These are the same helpers the member-facing network visualiser uses, exposed on the admin API for scripting and support tooling. - The list endpoint does not work. It is meant to return the users who hold a
binary node, and its filter reads
mlmBinaryNode IS NOT NULLon the referral rows — butmlmBinaryNodeis neither a column ofmlm_referral(id,referrerId,referredId,status,createdAt,updatedAt,deletedAt) nor the name of the association, which is aliasednode. The column named in the query does not exist, so the request errors instead of returning a list. Nor does it read the MLM system setting at all: this is a broken query on every install, not aBINARY-only endpoint returning an empty array on the others. Do not build tooling on it.
On a BINARY install, asking for a user who has no referral record at all
creates one — a self-referral (referrerId = referredId, status ACTIVE) — and
a root binary node to hang it from. This is the legacy backfill for users who
registered before MLM was enabled, and it runs on a GET.
It is harmless in normal use and it is why a user you merely looked at can
appear in mlm_referral afterwards. Do not sweep this endpoint across your user
table to "warm" anything.
Approving and rejecting, and what happens to the tree
There is no approve or reject control on this screen. A referral's status is
changed from the Referrals list, through the row's Edit form, which needs
edit.affiliate.referral.
Two different endpoints can change a referral's status, and they behave differently. This matters if you script against the API or if you are trying to explain a tree that does not match the list.
| Endpoint | Used by | Tree effect |
|---|---|---|
PUT /api/admin/affiliate/referral/{id} |
The admin Edit form | Rebuilds the node only when the referrer or the referred user changes. Changing status alone touches no node. |
PUT /api/admin/affiliate/referral/{id}/status |
API callers, one id | Records the status, then places the referral in the tree on ACTIVE or removes it on REJECTED |
PUT /api/admin/affiliate/referral/status |
API callers, bulk | No tree work at all. It writes the status for every id and stops |
On the single-id status endpoint, approving creates the binary or unilevel node: it finds the sponsor's node, runs a cycle check that refuses a referral whose referred user is already an ancestor (409, "Referral loop detected"), and if both of the sponsor's child slots are full it walks the tree breadth-first for the nearest free slot — that is spillover. Rejecting detaches the node, clears the parent's freed child slot, and re-parents the node's children to its own parent so a rejection cannot orphan a live sub-tree.
PUT /api/admin/affiliate/referral/status is a status write and nothing else —
it never calls the placement or removal helper. On a BINARY or UNILEVEL
install that means a bulk approval sets every referral to ACTIVE and
creates no node, so none of them appears in any tree view; and a bulk
rejection leaves each node in place, still occupying the slot it was given
and still blocking spillover into it.
There is no warning and no log line to find afterwards, because no tree update
was attempted to fail. If you script approvals, call the single-id endpoint per
referral. If a bulk call has already run, re-issue the same status one id at a
time through /{id}/status — placement returns early when the node already
exists, so it is safe to repeat.
The single-id status endpoint saves the decision first and does the tree work
afterwards, in a guard that logs and warns rather than rolling back. So a
referral can be ACTIVE in the list and absent from this screen's Network table.
If an approval does not appear in the diagram, that is a placement error in
the backend log, not a lost approval — search pm2 logs backend for
Referral <id> set to ACTIVE but the tree update failed. Re-approving through
the single-id status endpoint is safe: placement returns early if the node
already exists. Note that the absence of that log line does not mean the tree
is fine — if the approval came through the bulk endpoint, nothing was logged
because nothing was attempted.
Under DIRECT there is no tree at all and none of this applies.
Nothing is back-paid. Activity by a referred user while their referral was
PENDING produced no reward and will not produce one afterwards. The
Pending Longer Than 7 Days card on
the Referrals analytics panel is the queue-age figure to watch.
What this screen is good for
Given the caveats above, three investigations it genuinely answers:
-
"My downline is wrong." Open the member's referral, read the Network tab, and compare the level column against what they were told. Most of these disputes are spillover being read as a demotion, and the answer is that placement does not change earnings.
-
A suspected ring. The Network tab plus the Top Referrers bar on the Referrals analytics panel. One account holding an implausible share of the referrals, with downline members whose own referral counts are zero and whose accounts registered within minutes of each other, is the shape to look for.
-
"Am I earning?" Total Referrals and Total Earnings on this screen, then the real ledger at
/admin/affiliate/reward. If the tiles show referrals and no earnings, the question is a condition question, not an affiliate one — go to The hourly reward evaluator and work its check order.
The Rewards tab versus the Rewards screen
/admin/affiliate/reward is the real reward ledger: every commission ever
created, with its condition, its referrer, its amount and its claimed flag, plus
an analytics panel and the payout-routing detail (rewardType,
rewardWalletType, rewardCurrency, rewardChain) in the view dialog. The
Rewards tab on this screen is a per-affiliate convenience that currently renders
nothing but row count.
Setting isClaimed = true credits no wallet, writes no transaction and records
no platform loss. The reward simply disappears from the member's claimable list.
Use it only to reconcile a reward you settled some other way — credit the wallet
through the admin wallet tools first, then mark it claimed so the two records
agree. Rewards and payouts covers the real
claim path, the payout threshold and the withdraw_affiliate KYC gate.
One more difference worth knowing: on the Rewards table you mark a reward
claimed through the row's Edit form — the isClaimed field — and the only
bulk action is delete. There is no Create button, even though a create
endpoint, a create form definition and a create.affiliate.reward permission
all exist. Creating a reward by hand — for a launch incentive or a goodwill
adjustment — is currently an API call, not a screen action.
Related: Running the programme · Permissions and access · API and data model