Notifications this addon sends
The nine notification families copy trading fires at leaders, followers and administrators — recipients, channels, priorities, deep links, which events never fire, and what you can and cannot reword.
Copy trading sends nine distinct families of notification. They are separate from the audit log: a notification is what the customer saw, an audit row is what happened. A notification can fail to deliver and leave no trace on the customer's side while the audit row is written regardless — that asymmetry is the reason this page exists.
All nine live in one file and all nine go through the platform notification service, so they inherit its per-user preferences, its channel filtering and its queueing.
The nine families
| Family | Recipient | Events |
|---|---|---|
notifyLeaderApplicationEvent |
The applicant / leader | APPLIED APPROVED REJECTED SUSPENDED ACTIVATED |
notifyLeaderNewFollower |
The leader | Somebody subscribed |
notifyLeaderFollowerStopped |
The leader | A follower stopped |
notifyFollowerSubscriptionEvent |
The follower | STARTED PAUSED RESUMED STOPPED FORCE_STOPPED |
notifyFollowerAllocationEvent |
The follower | CREATED FUNDS_ADDED FUNDS_REMOVED INSUFFICIENT_BALANCE |
notifyFollowerTradeEvent |
The follower | COPIED CLOSED FAILED PROFIT LOSS |
notifyFollowerRiskEvent |
The follower | DAILY_LOSS_LIMIT POSITION_SIZE_LIMIT AUTO_PAUSED AUTO_STOPPED |
notifyProfitShareEvent |
Leader or follower | EARNED RECEIVED DISTRIBUTED |
notifyCopyTradingAdmins |
Every administrator | LEADER_APPLICATION SUSPICIOUS_ACTIVITY HIGH_LOSS_FOLLOWER LEADER_SUSPENDED |
Copy trading also sends four notifications outside this system, through the
core createNotification helper — the automatic daily-limit resume, the daily-loss
pause, the backstop settler's trade-closed message, and the admin reactivation
message. Those are in-app only.
Leader application events
The five events all carry relatedId = the leader id and deep-link to
/copy-trading/leader/me, except suspension, which links to /support.
| Event | Type | Priority | Channels | Fired by |
|---|---|---|---|---|
APPLIED |
SYSTEM |
NORMAL | IN_APP, PUSH, EMAIL | POST /api/copy-trading/leader/apply |
APPROVED |
SYSTEM |
NORMAL | IN_APP, PUSH, EMAIL | The approve route, and apply when auto-approve is on |
REJECTED |
ALERT |
HIGH | IN_APP, PUSH, EMAIL | POST /api/admin/copy-trading/leader/{id}/reject |
SUSPENDED |
ALERT |
URGENT | IN_APP, PUSH, EMAIL | POST /api/admin/copy-trading/leader/{id}/suspend |
ACTIVATED |
SYSTEM |
NORMAL | IN_APP, PUSH | Never fired — see below |
The rejection reason you type into the reject dialog is carried into the message the applicant reads: "Your leader application was not approved. Reason: …". With no reason supplied the message falls back to "Please contact support for details." The same is true of a suspension reason, which the suspend endpoint makes mandatory.
Write those reasons as if the customer will read them, because they will.
POST /api/admin/copy-trading/leader/{id}/activate does not call this family at
all. It sends its own in-app-only notification titled "Copy Trading Account
Reactivated", linking to /user/copy-trading/leader.
So a reactivated leader gets no email and no push, and lands on a link that does not resolve to a page (see the deep-link table below). Tell a reinstated leader directly.
Follower subscription events
Deep-link to /copy-trading/follower/{followerId}, except FORCE_STOPPED, which
links to /support.
| Event | Type | Priority | Channels | Fired by |
|---|---|---|---|---|
STARTED |
SYSTEM |
NORMAL | IN_APP, PUSH, EMAIL | POST /api/copy-trading/follower/follow |
PAUSED |
ALERT |
NORMAL | IN_APP, PUSH, EMAIL | The follower's own pause, the admin pause, and every follower of a suspended leader |
RESUMED |
SYSTEM |
NORMAL | IN_APP, PUSH, EMAIL | The follower's own resume |
STOPPED |
ALERT |
HIGH | IN_APP, PUSH, EMAIL | The follower's own stop |
FORCE_STOPPED |
ALERT |
HIGH | IN_APP, PUSH, EMAIL | Never fired — see below |
STOPPED and FORCE_STOPPED both load the subscription's statistics before
sending, so the message costs an extra computation; everything else is a plain read.
The suspension gap you have to plan around
This is the single most consequential behaviour on this page.
Suspending a leader pauses every follower behind them and sends each one a
PAUSED notification reading "Leader suspended: URGENT alert. Administrators get a LEADER_SUSPENDED alert.
Reactivating that leader sends nothing to any follower, and does not resume them.
resetCopyTradingDailyLimits — the only job that lifts a pause automatically —
deliberately skips any follower whose leader is not ACTIVE, and even then only
lifts a pause whose most recent audit action is DAILY_LOSS_LIMIT_REACHED from a
previous UTC day. A pause caused by a suspension is recorded against the leader,
not the follower, so nothing will ever lift it.
Every one of those followers stays paused, with money still sitting in their
COPY_TRADING wallet, until each of them resumes it or you resume it for them at
/admin/copy-trading/follower. Plan the reactivation announcement before you
suspend a leader with a book.
The bulk endpoint is worse in one specific way. POST /api/admin/copy-trading/leader/bulk/status
with status: "SUSPENDED" pauses every affected follower exactly as the single
suspend does, but sends no notification to anybody — not the leaders, not the
followers, not administrators. It only writes BULK_SUSPENDED audit rows. If you
use it on a backlog, do the customer communication yourself.
The families with no call sites
Three of the nine are written, exported and unreachable:
| Family | State |
|---|---|
notifyFollowerAllocationEvent |
No call site anywhere. Adding, removing or exhausting allocation funds notifies nobody |
notifyFollowerRiskEvent |
No call site. The daily-loss pause message a follower actually receives comes from the cron's own createNotification, with different wording |
notifyProfitShareEvent (RECEIVED, DISTRIBUTED) |
Only EARNED fires, from binary settlement, to the leader. Followers are never told a profit share was taken |
notifyFollowerTradeEvent fires only on the binary path — COPIED when a
binary copy is placed, then PROFIT, LOSS or CLOSED at settlement according to
the sign of the P&L. FAILED has no call site. Spot copies produce no per-trade
notification from this family at all; a spot follower is told only when the backstop
settler closes their trade, through a separate in-app message.
Do not build an operating procedure on a customer being told about an allocation change, a risk pause, or a profit share deduction.
Channels, priorities and preferences
Requested channels are filtered per user before anything is sent. The defaults on a user who has never touched their notification settings are:
| Channel | Default | Effect |
|---|---|---|
IN_APP |
Always allowed | The bell, plus a live WebSocket push when they are online |
EMAIL |
On | Queued for async delivery |
PUSH |
Off | Silently dropped unless the user has enabled push and registered a device |
SMS |
Off | Copy trading never requests it |
So the practical shipped behaviour is in-app plus email, with push reaching only
users who opted in. Every family requests IN_APP and PUSH; the leader-application
and follower-subscription families additionally request EMAIL, as do the two
leader-facing follower events. The allocation, trade, risk, profit-share and admin
families are in-app and push only — they are never emailed.
priority (LOW / NORMAL / HIGH / URGENT) only sets the email queue's job
priority and is stored on the row. type (SYSTEM or ALERT) is the axis that
customers can filter on: a user who has switched the system notification type off
in their preferences will not receive the SYSTEM half of this list at all. Note
that copy trading never tags these with the COPY_TRADING notification type, so a
per-product opt-out is not available to them.
What you can and cannot reword
Templates are edited at Admin → System → Notifications → Templates
(/admin/system/notification/template), and the email renderer looks a template up
by name in notification_template, only using rows where the email flag is on.
The copy-trading notification code names ten email templates —
copyTradingLeaderApplication, copyTradingLeaderApproved,
copyTradingLeaderRejected, copyTradingLeaderSuspended, copyTradingNewFollower,
copyTradingFollowerStopped, copyTradingSubscriptionStarted,
copyTradingSubscriptionPaused, copyTradingSubscriptionResumed and
copyTradingSubscriptionStopped.
None of them are seeded, and the email channel reads a differently-named field than the one copy trading populates, so the lookup never happens. Every copy-trading email is therefore rendered by the generic fallback builder from the notification's own title and message — the same two strings the in-app bell shows.
You cannot change that wording from the admin panel. Creating rows with those names will not take effect either. The only operator-controlled text in any of these messages is the reason you type on a reject, a suspend or an admin stop.
Everything else — the titles, the sentences, the deep links — is in
copy-trading/utils/notifications.ts and changing it is a code change.
Deep links: most of them do not resolve
The links carried on these notifications were written against a route layout that does not match the shipped frontend. Verify before you tell a customer to "click the link in the notification".
| Link used | Resolves |
|---|---|
/copy-trading/subscription |
Yes — used by the two cron notifications |
/support |
Yes — used by suspension and force-stop |
/admin/copy-trading, /admin/copy-trading/leader/{id}, /admin/copy-trading/follower/{id}, /admin/copy-trading/audit |
Yes — the administrator links are all correct |
/copy-trading/leader/me |
No — matches the leader/[id] route with the literal id me and renders a missing leader |
/copy-trading/leader/followers, /copy-trading/leader/earnings |
No — same problem |
/copy-trading/follower/{id}, /copy-trading/follower/{id}/trades, /copy-trading/follower/history |
No — there is no follower route |
/copy-trading/earnings, /copy-trading/subscriptions |
No — the real page is /copy-trading/subscription |
/user/copy-trading/leader |
No — used by the admin reactivation message |
The notification text itself is accurate in every case; it is only the destination
that is wrong. Point customers at /copy-trading/dashboard and
/copy-trading/subscription.
The administrator channel
notifyCopyTradingAdmins resolves its recipients by role permission: every user
whose role carries access.copy_trading. Add that permission to a role and its
members start receiving these; remove it and they stop. There is no separate
notification opt-in list.
| Event | Priority | Link | Fired by |
|---|---|---|---|
LEADER_APPLICATION |
NORMAL | /admin/copy-trading/leader/{id} |
A new leader application |
LEADER_SUSPENDED |
NORMAL | /admin/copy-trading/leader/{id} |
The single suspend endpoint |
SUSPICIOUS_ACTIVITY |
HIGH | /admin/copy-trading/audit |
No call site |
HIGH_LOSS_FOLLOWER |
HIGH | /admin/copy-trading/follower/{id} |
No call site |
All are sent as ALERT, in-app and push only. There is no administrator email for
any copy-trading event.
copyTradingAutoApproveLeaders makes the apply endpoint skip
notifyCopyTradingAdmins entirely — the applicant is told they were approved, and no
administrator is told anything. That is not a bug, it is the point of the setting,
but it means your only record of a new leader is the audit log and the pending-free
leader list.
If you run with auto-approve on, put a recurring check of /admin/copy-trading/leader
in your routine. See Settings.
Failures are swallowed
Every one of the nine families wraps its whole body in try/catch, logs the
failure under the copy-trading module, and returns normally. The admin fan-out
catches per-recipient as well, so one bad administrator row does not stop the rest.
That is the right behaviour — a failed notification must never roll back a suspension or a subscription — but it has an operational consequence:
If SMTP credentials are wrong, if the notification queue is not draining, or if the email channel's configuration check is failing, nothing on any copy-trading screen will tell you. Suspensions still complete, subscriptions still start, audit rows still land, and every admin action reports success.
The signals are outside this addon: the backend log, and the email queue on Admin → System → Notifications. Check both after any change to mail configuration, and treat "customers say they got no email" as a mail-infrastructure report rather than a copy-trading one.
One more consequence of how these are keyed: every family builds its idempotency key with the current timestamp appended, so the notification service's duplicate suppression can never match. Two suspensions of the same leader a second apart produce two notifications. This is not a setting.
Notifications versus the audit trail
They answer different questions and they are not interchangeable.
| Notification | copy_trading_audit_logs |
|
|---|---|---|
| Records | What the customer was told | What the platform did |
| Written when delivery fails | The in-app row still lands; email and push do not | Always |
| Carries the acting admin | No | Yes, in adminId |
| Carries old and new values | No | Yes, as JSON |
| Carries the reason | Only inside the message text | Yes, in its own column |
| Survives the customer deleting it | No | Yes |
When you are reconstructing an incident, the audit log at /admin/copy-trading/audit
is the record. Use the notification only to answer "did we tell them, and what did we
say". Remember that two spellings of the follower entity type are in use —
copyTradingFollower from the engine and FOLLOWER from the user-facing routes — so
filter for both.