Service Architecture · Internal Reference

ZuluTrade CRM at a glance

A top-down walkthrough of every service that powers the ZuluTrade platform — from user sign-up, to broker connection, to live trade signal routing. Click any module card below to jump to its detailed breakdown.

System at a Glance
Zulutrade CRM Application Server User Onboarding Sign-up · SSO · sessions Click to jump to User Onboarding detail Account Connection Leaders · copiers · broker connect Click to jump to Account Connection detail Copier Flow Signup → subscription → start copying Click to jump to Copier Flow lifecycle narrative Leader Flow Application → admin approval → registered Click to jump to Leader Flow lifecycle narrative Trading Flow Signal pipeline · ingest → execute Click to jump to Trading Flow detail Bridges ACT Bridge · node-middleware Click to jump to Bridges detail Analytics Trade history → ClickHouse → stats Click to jump to Analytics detail Rewards Copy-trading earnings · payouts Click to jump to Rewards detail Badges Gamification · milestones → history Click to jump to Badges detail Other Services Notifications · subs · rewards · community Click to jump to Other Services detail Admin admin-express · admin-service · admin-mysql Click to jump to Admin detail Ops / Data Pipeline data-pipeline · import-history Click to jump to Ops / Data Pipeline detail Data & Infra Storage strategy · databases · caches Click to jump to Data & Infra detail Inventory Full service runtime reference Click to jump to Inventory reference ↓ click any module to jump to its detail Databases / Stores MariaDB PostgreSQL Redis ⚠ MongoDB ClickHouse RabbitMQ 🐇 pink = persisted stores orange = message bus

Click any module to jump to its detailed section below. Each section lists its external integrations.

Module 01 · Application Layer

User Onboarding & Login

How a new user goes from zero to an authenticated session. The flow starts at client-express, writes a lead into legacy MariaDB, fires an OTP email via communication → Sendgrid, then (on OTP submit) creates the canonical user record in both MariaDB and Postgres and broadcasts a userRegister event on RabbitMQ. SSO logins skip the OTP step entirely.

🔗 External integrations
Apple / Google SSO Sendgrid
🧠 Design principle
Onboarding has two entry paths — email + OTP and SSO (Google / Apple) — but both converge on a single canonical user record in Postgres zulu3. Lead + session rows in MariaDB are a legacy parity mirror, not a source of truth. The target shape is: FE → client-express (gateway) → one owning service (today users-service, proposed identity-service) → Postgres write → userRegister event on RabbitMQ. Every downstream reaction — ACT customer creation, badge progress, welcome notifications, subscription bootstrap — is an async consumer of userRegister. OTP dispatch is async via communication → Sendgrid; nothing third-party should sit on the 200-response critical path.
User Onboarding / Login · combined FE (Browser) client-express auth-service Apple / Google SSO flavour-service client-mysql communication Sendgrid MariaDB (legacy) zulu3_biz · lead · user session 🐇 userRegister (RabbitMQ) users-service zulu3 (Postgres) register SSO verify country · flavour id lead / user OTP email persist POST user JWT / session
Email + OTP sign-up · target flow FE (Browser) client-express users-service Postgres zulu3 🐇 email.otp.requested notifications-service Sendgrid 🐇 userRegister connector-hub ActTrader register upsert userRegister

How it works (target)

1. Register. FE posts to client-express, which forwards to users-service for a pending user row in Postgres.

2. OTP dispatch. users-service publishes email.otp.requested on RabbitMQ; notifications-service consumes and calls Sendgrid. OTP send does not block the 200 response (today it is synchronous and routed through a separate communication service — that service will be retired and its transactional-email responsibilities folded into notifications-service; see Architecture Improvements below).

3. Verify. FE submits the OTP; users-service promotes the pending row to active, issues the JWT, publishes userRegister.

4. Fan-out. connector-hub consumes userRegister and requests temp-trader to create the mirror ACT customer. temp-trader (NestJS + RabbitMQ, after upgrade) calls ActTrader and publishes ACTCustomer; users-service consumes it and writes ACT details back to the user row.

SSO sign-up (Google / Apple) FE (Browser) client-express auth-service Apple / Google SSO users-service Postgres zulu3 🐇 userRegister OAuth verify upsert

How it works

SSO skips the OTP step because the provider has already verified the email. client-express forwards to auth-service, which talks to Apple or Google. On success, users-service upserts the user row in Postgres and fires userRegister — from here the flow is identical to the Email + OTP fan-out.

Flavour / country lookup FE (Browser) client-express flavours-service Postgres zulu_app_config /flavour · /country

How it works

FE asks flavours-service for the country list and resolves the white-label flavour from the Origin header. Target: keep the service and expand it into the canonical home for per-domain / per-tenant configuration (white-label toggles, feature flags, locale / regulatory rules). All FE access routes through client-express rather than hitting the service directly (see Architecture Improvements).

Services

client-express
Public HTTP gateway. All browser traffic enters here and is fanned out to internal services.
client-mysql
Thin adapter over the legacy MariaDB databases (zulu3_biz, zulu3_session). Used only for login and legacy registration compatibility.
users-service
Canonical user identity. Owns the user record in zulu3 Postgres. All user lookups go here post-onboarding.
auth-service
Issues sessions / JWTs. Talks to Apple & Google SSO providers for federated login.
flavour-service
Domain-level config: country list, white-label flavour toggles, feature flags per deployment.
communication
Owns transactional email. Reads templates from zulu3_communication and dispatches via Sendgrid.

Sign-up Sequence — Email + OTP

  1. Register request — FE submits the registration form to client-express.
  2. Lead createdclient-express writes a lead record into MariaDB via client-mysql.
  3. OTP emailclient-express triggers communication service, which dispatches the OTP email through Sendgrid.
  4. OTP verified — user submits the OTP; client-expressclient-mysql creates the user record in MariaDB. This record is later used by client-express to issue JWT tokens and manage login sessions / auth.
  5. Postgres mirrorclient-express sends a POST to users-service, which creates the matching user record in the Postgres zulu3 database.
  6. Event broadcastusers-service publishes a userRegister event on RabbitMQ so every downstream service can react.
  7. ACT customer profileconnector-hub listens to the userRegister event and passes it on to temp-trader, which creates an ACT customer profile. On successful profile creation, an ActCustomer event is published on RabbitMQ; users-service consumes it and stores the details in the Postgres user profile.

Sign-up Sequence — SSO (Google / Apple)

  1. client-express forwards the SSO request directly to auth-service.
  2. The provider verifies email ownership. On success, client-express creates the user if it does not already exist (skipping the OTP step — the provider has already verified the address) and issues the session without requiring a password.

Flavour / Country

On the registration form, flavour-service provides the country list and tells the FE which flavour ID is linked to the current domain, so the right white-label configuration and feature flags are applied.

Architecture improvements

Target-state direction for the onboarding module — narrow the sign-up path to a single source of truth, get the gateway out of the orchestration business, and move third-party latency (Sendgrid, ActTrader, OAuth providers) off the 200-response path. Each tab groups the proposal a different way: the decisions we're committing to, the principles that back them, and the concrete risks each decision resolves.

Extract → identity-service
Extract identity responsibilities from users-service (or evolve it under a clearer name) so it becomes the single writer of user identity on the sign-up path. JWT issuance moves here and reads the same Postgres row it just wrote.
Shrink → client-express
Gateway duties only — routing, JWT validation, rate-limit, OpenAPI, response envelope. No orchestration, no direct DB writes. The seven-step sign-up sequence inside the register request collapses into one hop.
Retire → client-mysql + MariaDB → PostgreSQL
Retire client-mysql entirely and migrate its MariaDB backing store (zulu3_biz, zulu3_session) to PostgreSQL. Freeze new plugins today; over time each of the 14 existing domains (users, biz-profile, deposits, withdrawals, bonuses, subscriptions, trading, IB, admins, reports, …) moves to its owning service with Postgres-native persistence. End state: no Node HTTP DAL in front of a database, no MariaDB, one relational store.
Shrink → auth-service (SSO only)
Extract the exports plugin + sync-trade-history + BullMQ workers + daily_balance_history cron into a new analytics-exporter-service. auth-service keeps only SSO, sessions/JWT, verification.
Expand → flavours-service
Keep the service and grow its scope. Beyond the current country list + domain-to-flavour-id lookup, it becomes the canonical home for per-domain / per-tenant configuration — white-label toggles, feature flags, locale and regulatory rules — so client-express and downstream services have a single config source. All FE access routes through the gateway, not direct to the service.
Async → OTP dispatch (via notifications-service)
Retire communication. notifications-service takes over transactional email (OTP, welcome, verification) alongside its existing push / in-app / SMS channels. users-service publishes email.otp.requested on RabbitMQ; notifications-service consumes and dispatches via Sendgrid. One service owns user-facing messaging end-to-end; Sendgrid latency / outages stop blocking registration.
Upgrade → temp-trader (NestJS + RabbitMQ)
Rebuild temp-trader on NestJS with RabbitMQ consumer + publisher. It consumes the ACT-customer-creation request from connector-hub, creates the ACT customer via ActTrader, and publishes ACTCustomer itself — as the architecture originally described. Removes today's mismatch where connector-hub has quietly taken over the publish role because temp-trader had no RMQ. Publisher/consumer contract test locks the boundary.
Idempotency keys
Every consumer of userRegister / ACTCustomer dedups on eventId via Redis (24 h TTL). Redeliveries become safe; duplicate publishes become safe. One shared helper, used everywhere.
Schema ownership — zulu3 Postgres
Enforce one schema per service inside shared zulu3. No cross-schema writes. Cross-service reads via published views or read-only APIs. Stops the distributed-monolith slide.
P1 Single source of truth for identity
User identity lives in one store — Postgres zulu3. MariaDB is a read-replica during transition. JWT reads the same row the writer just wrote. No distributed transactions, no dual-write race.
P2 Gateways don't orchestrate
client-express owns routing, JWT, rate-limit, CORS, observability — nothing else. Business steps live in the owning service. A sign-up request is one gateway hop + one business hop + events for fan-out.
P3 Third parties stay off the critical path
Sendgrid, Apple, Google, ActTrader all respond eventually. Nothing third-party sits on the 200-response path. Outbox + event consumers absorb the latency.
P4 At-least-once + idempotent consumers
Every event carries an eventId. Every consumer dedups. Redeliveries, duplicate publishes, and retries become safe by construction — we don't chase exactly-once at the infra level.
P5 Names match responsibilities
A service called client-mysql should not host 14 domain plugins. A service called auth-service should not export trade history. Truth-in-names, or rename.
P6 One writer per schema / topic
Shared zulu3 Postgres gets one-schema-per-service. Every RMQ topic has exactly one producer. Cross-service reads are explicit (views / APIs); cross-service writes don't exist.
P7 Contract tests at every seam
Every publisher/consumer pair has a JSON-schema contract. Drift fails CI instead of on-call. The ACTCustomer mislabelling is the kind of bug this makes impossible.
P8 Config > service for static data
Country lists and white-label flavours don't need a pod. Edge-served JSON, reloaded on change. Reserve service boundaries for things that own state or orchestrate logic.
IDRisk todayResolved by
R1Dual identity store — MariaDB + Postgres with no transactional glue. A user can log in but have no canonical profile if the second write fails.Postgres becomes the single writer; MariaDB is a background replica. JWT is issued from Postgres. See ADR-01.
R2client-express is gateway + orchestrator + business logic. p99 = Σ(downstream p99); orphan rows on partial failure.Strip orchestration. Single-hop sign-up into the owning service. See ADR-02.
R3client-mysql named as infrastructure hosts 14 domain plugins over a MariaDB that's off the strategic path. Running two relational engines doubles ops surface and keeps the legacy store alive indefinitely.Retire client-mysql; migrate MariaDB (zulu3_biz, zulu3_session) to PostgreSQL; each domain moves to its owning service with Postgres-native persistence. See ADR-03.
R4auth-service owns unrelated exports + cron + BullMQ workers. A crashing analytics worker takes down login.Extract to analytics-exporter-service. auth-service keeps only SSO + sessions + verification. See ADR-04.
R5temp-trader was documented as the ACTCustomer publisher but has no RabbitMQ today; connector-hub quietly took over the role. Doc / code drift — on-call debugs in the wrong service.Upgrade temp-trader to NestJS with RabbitMQ (consumer + publisher). It owns ACT customer creation end-to-end and publishes ACTCustomer itself; connector-hub stops playing that role. Publisher/consumer contract test on both sides. See ADR-05.
R6No idempotency on userRegister / ACTCustomer consumers. Redeliveries mutate the same row repeatedly.Shared Redis-backed dedup helper (SET NX PX 86400000) on every consumer. See ADR-06.
R7Shared zulu3 Postgres written by 5+ services = distributed monolith. Schema migrations cross service boundaries.One schema per service; least-privilege Postgres roles; cross-service reads via views / APIs. See ADR-07.
R8Sendgrid latency / outage blocks registration because OTP send is synchronous, and transactional email is split off into a dedicated communication service.Retire communication; fold transactional email into notifications-service. users-service publishes email.otp.requested on RabbitMQ; notifications-service consumes and dispatches via Sendgrid. Registration returns 200 once the event is persisted. See ADR-08.
R9FE calls flavours-service directly, bypassing the gateway (no JWT, no rate-limit, no observability).All FE access to flavour / config data is routed through client-express. flavours-service stays and grows to own per-domain / per-tenant configuration end-to-end. See ADR-10.

Click an endpoint to expand its request & response. Envelope styles differ per service — noted inline. Scoped to the onboarding / registration / SSO path; full API reference lives in api_docs/onboarding-flow.md.

zulu3.0-client-mysql · MariaDB DAL — registration step writes + OTP verify

POST /create/register/step/one client-mysql · biz-profile/routes/index.js:74
Handler: registration step 1 (biz-profile variant) Auth: JWT Persists to: MariaDB zulu3_biz
Request · body
{
  "fname": "string",
  "mname": "string",
  "lname": "string",
  "dob": "YYYY-MM-DD",
  "mobile_no": "string",
  "address1": "string",
  "address2": "string?",
  "post_code": "string?",
  "residence_country": "string?",
  "city": "string?",
  "currency": "string?",
  "step": "number?"
}
Response · 200
{ "info": "OK", "msg": "success", "data": { /* session update + insert ids */ }, "errors": [] }
POST /api/otpverify client-mysql · user-operations/routes/index.js:437
Handler: verify OTP Auth: JWT
Request · body
{
  "otp": "string",     // 4-6 digits
  "request_id": "uuid?"
}
Response · 200
{ "info": "OK", "msg": "success", "data": { "verified": true }, "errors": [] }
Response · NOK
{ "info": "NOK", "msg": { "code": "OTP_INVALID", "msg": "Invalid OTP" }, "data": {}, "errors": [] }
POST /api/resendotp client-mysql · user-operations/routes/index.js:468
Handler: resend OTP Auth: JWT
Request · body
{ "request_id": "uuid" }
Response · 200
{ "info": "OK", "msg": "resent", "data": { /* new request id / expiry */ }, "errors": [] }

zulu3.0-users-service · canonical user record · publishes userRegister

POST /users/event/register users-service · users/routes/index.ts:27
Handler: userRegister Auth: public (service-to-service via client-express proxy) Validator: UsersValidators.userRegister() Publishes: userRegister + B-MAIL-V
Request · body
{
  "user": {
    "user_id": 123456,
    "email": "alice@example.com",
    "firstname": "Alice",
    "lastname": "Smith",
    "gender": "F",
    "dob": "1990-01-15",              // "0000-00-00" normalised → "1900-01-01"
    "country": "IN",
    "timezone": "Asia/Kolkata",
    "mobile_code": "+91?",
    "mobile_no": "9999999999?"
  }
}
Response · 200
{
  "Data": { /* registerUser result */ },
  "Message": "UserRegister Event Published Successfully",
  "HttpCode": 200
}
POST /users/event/login users-service · users/routes/index.ts:28
Handler: userLogin Publishes: userLogin
Request · body (tolerant)
{
  "user_id": 123456,              // or profile.user_id / user.user_id / user.profile.user_id
  "email": "alice@example.com",
  "device_token": "string?"
}
Response · 200
{ "Data": {}, "Message": "UserLogin Event Published Successfully", "HttpCode": 200 }
GET /users/phone-verification users-service · users/routes/index.ts:37
Handler: trigger phone OTP using user's saved phone Auth: public (requires user_id)
Request · query
?user_id=123456
Response · 200
{ "Data": { /* request_id, expiry */ }, "Message": "Phone Verification Requested Successfully" }
Response · 4xx
{ "ErrorCode": "INVALID_USER_ID" | "PHONE_NUMBER_NOT_SET" | "USER_NOT_FOUND" }

zulu3.0-auth-service · SSO + phone/email OTP + magic link

GET /apple/url auth-service · apple-auth.controller.ts:16
Purpose: Apple OAuth authorisation URL Auth: public
Request · query
?state=<csrf-state>
Response · 200
{ "url": "https://appleid.apple.com/auth/authorize?..." }
POST /apple/redirect auth-service · apple-auth.controller.ts:30
Purpose: Apple form_post callback — prefers id_token, falls back to exchanging code Auth: public (validates id_token itself)
Request · body
{
  "id_token": "jwt?",
  "code": "string?",
  "state": "string?"
}
Response · 200
{ "profile": { /* Apple user payload (email, sub, name) */ } }
Response · 400
"Missing id_token or code" | "Invalid Apple id_token" | "Failed to authenticate with Apple…"
POST /google/onetap auth-service · google-auth.controller.ts:30
Purpose: verify a Google One Tap id_token against Google's JWKS Auth: public
Request · body
{ "credential": "google-id-token-jwt" }
Response · 200
{ "profile": { /* decoded Google id_token payload (email, sub, name, picture) */ } }
POST /verification/phone/request auth-service · verification.controller.ts:18
DTO: PhoneVerificationRequestDto
Request · body
{
  "phoneE164": "+919999999999",     // /^\+[1-9]\d{1,14}$/
  "policyKey": "string",
  "fullName": "string?",             // ≤ 200 chars
  "remarks": "string?"
}
Response · 200
{ "data": { "requestId": "uuid", "expiresAt": "ISO-8601" } }
POST /verification/phone/verify auth-service · verification.controller.ts:25
DTO: PhoneVerificationVerifyDto
Request · body
{
  "requestId": "uuid",
  "otp": "string"      // 4-6 digits
}
Response · 200
{ "data": { "verified": true } }
POST /verification/email/request auth-service · verification.controller.ts:39
DTO: EmailVerificationRequestDto Modes: OTP · magic link · both
Request · body
{
  "email": "alice@example.com",
  "mode": "otp" | "magic" | "both",
  "policyKey": "string",
  "landingBaseUrl": "string?",        // required for magic / both
  "fullName": "string?"
}
Response · 200
{ "data": { "requestId": "uuid", "expiresAt": "ISO-8601" } }
POST /verification/email/verify auth-service · verification.controller.ts:46
DTO: EmailVerificationVerifyDto
Request · body
{
  "requestId": "uuid",
  "otp": "string"      // 4-6 digits
}
Response · 200
{ "data": { "verified": true } }

zulu3.0-flavours-service · country list + per-domain flavour lookup

GET /config/country/list flavours-service · onboardingData/routes/index.ts:13
Purpose: full country dropdown list for the registration form Auth: public
Response · 200
{ "Data": [ { "id": 1, "name": "India", "iso": "IN", "dial_code": "+91" }, ... ], "Message": "Country List Retrieved Successfully" }
GET /config/flavour flavours-service · onboardingData/routes/index.ts:15
Purpose: resolve the white-label flavour-id from the request Origin header Auth: public
Request · headers
Origin: https://<domain>
Response · 200
{ "Data": { "flavour_id": 3, "properties": { /* white-label toggles, feature flags, locale rules */ } }, "Message": "Flavour Retrieved Successfully" }

zulu3.0-communication · being retired — folds into notifications-service

POST /communication/email communication · email/routes/index.ts:17
Purpose: transactional email (OTP / welcome / verification) via SendGrid Target state: consumer moves to notifications-service via email.otp.requested RMQ event
Request · body
{
  "emailTrigger": "OTP_REGISTRATION" | "WELCOME" | "PASSWORD_RESET" | ...,
  "toEmail": "alice@example.com",
  "data": { /* merge fields for the template */ }
}
Response · 200
{ "Data": { "delivered": true, "messageId": "sg_..." }, "Message": "Email Dispatched Successfully" }

zulu3.0-temp-trader · ACT customer creation — HTTP today, NestJS + RMQ target

POST /actTrader/customer/create temp-trader · actTrader/routes/index.js
Caller: connector-hub on userRegister fan-out Target state: triggered by RMQ event create.act.customer; response becomes ACTCustomer publish
Request · body
{
  "user_id": 123456,
  "email": "alice@example.com",
  "firstname": "Alice",
  "lastname": "Smith",
  "country": "IN",
  "currency": "USD"
}
Response · 200
{
  "info": "OK",
  "data": {
    "act_account_id": "ACT-0001234",
    "act_platform": "s281",
    "act_username": "alice_s281"
  }
}

Async handoffs on the onboarding path · not HTTP — RabbitMQ events

EVENT 🐇 userRegister users-service → connector-hub · badges-service · notifications-service
Publisher: users-service (after POST /users/event/register) Queue / binding: act_customer · topic userRegister
Payload
{
  "user_id": 123456,
  "email": "alice@example.com",
  "firstname": "Alice",
  "lastname": "Smith",
  "country": "IN",
  "timezone": "Asia/Kolkata",
  "flavour_id": 3
}
EVENT 🐇 ACTCustomer temp-trader (target) → users-service
Publisher (today): connector-hub (drift) Publisher (target): temp-trader after NestJS + RMQ upgrade Queue / binding: usersService · topic ACTCustomer
Payload
{
  "user_id": 123456,
  "act_account_id": "ACT-0001234",
  "act_platform": "s281",
  "act_username": "alice_s281"
}
EVENT 🐇 email.otp.requested users-service (proposed) → notifications-service
Status: proposed — part of the async-OTP target (retire communication) Queue / binding: notifications queue · topic email.otp.requested
Payload
{
  "requestId": "uuid",
  "userId": 123456,
  "email": "alice@example.com",
  "otp": "123456",
  "templateId": "OTP_REGISTRATION"
}
EVENT 🐇 B-MAIL-V users-service → badges-service
Publisher: users-service (during POST /users/event/register) Queue / binding: badges · topic B-MAIL-V
Payload
{
  "userId": 123456,
  "email": "alice@example.com",
  "verifiedAt": "ISO-8601"
}
⚠ Tech Debt
MariaDB (zulu3_biz, zulu3_session) is the legacy store kept only for login & registration parity. All new user state should be written to Postgres zulu3. A full cutover is on the roadmap.
↑ Back to overview
Module 02 · App + ACT/FTL Layer

Account Connection

Once the user is registered & logged in, they must choose to be a Leader or Copier. FE asks connector-hub for available platforms: a Leader can connect MT4, MT5, ACT, Ctrade, Telegram or Discord; a Copier can connect MT4, MT5, ACT or Ctrade. connector-hub issues a unique ID, mounts the signal source on the relevant bridge (act-bridge / social-bridge), creates a mirror ACT account via temp-trader, and — for a Leader — sets up the Leader profile on FTL via leader-service.

🔗 External integrations
FTL S281 (demo) ACT Brokers K8s cluster node-middleware
Account Connection · combined landscape FE (Browser) client-express connector-hub 🐇 sessions queue social-bridge (node-middleware) act-bridge K8s MT pod Ctrade TG / Discord ACT broker 🐇 ActCustomer event temp-trader Leader / Copier profile leader-service copier-service FTL users-service Postgres zulu3 pick platform publish state MT / Ctrade / TG / DC ACT state events ACT account consume Leader Copier

How it works

After login, FE asks connector-hub for the platforms available to this user's role. Leader can pick from MT4, MT5, ACT, Ctrade, Telegram, Discord. Copier can pick from MT4, MT5, ACT, Ctrade.

Common steps for any platform:

1. connector-hub issues a unique account ID and forwards the connection request to the relevant bridge — social-bridge (node-middleware) for MT4/MT5/Ctrade/TG/Discord, act-bridge for ACT.
2. The bridge mounts the signal source and publishes state transitions on the sessions queue ({connector_connected, synced} progressing from false/falsetrue/falsetrue/true).
3. connector-hub consumes those state events. Once {connector_connected: true, synced: true} arrives, it requests temp-trader to create a mirror ACT customer account (for trade mirroring).
4. temp-trader creates the ACT account and publishes an ActCustomer event on RabbitMQ; users-service consumes it and stores the details in the Postgres user profile.
5. If the role is Leader, leader-service creates the Leader profile on FTL. If the role is Copier, copier-service wires the copy relationship.

MT4 / MT5 · account connection User (FE) connector-hub social-bridge (node-middleware) credentials issue address + connect req 🐇 sessions queue { connector_connected: false, synced: false } consume K8s MT4/MT5 terminal pod create pod 🐇 sessions queue { connector_connected: true, synced: false } ping container → pull profile 🐇 sessions queue { connector_connected: true, synced: true } connector-hub temp-trader ACT account consume synced request pull symbols from social-bridge → symbol mapping leader-service → FTL if Leader

How it works

1. Submit credentials — User submits MT4/MT5 credentials from FE. connector-hub issues an address and sends a connection request to social-bridge (node-middleware).

2. Initial state — social-bridge publishes an event on the sessions queue with { connector_connected: false, synced: false }. connector-hub consumes it to track state.

3. Pod creation — social-bridge creates an MT4/MT5 terminal pod on the K8s cluster and publishes { connector_connected: true, synced: false }.

4. Profile sync — social-bridge pings the MT container, pulls profile data, and publishes { connector_connected: true, synced: true }.

5. Mirror account — connector-hub requests an ACT account via temp-trader. On success, symbols are pulled from social-bridge and symbol mapping is performed.

6. Role-specific step — If Leader, leader-service sets up the Leader profile on FTL. If Copier, copier-service wires the copy relationship to the chosen leader(s).

Ctrade · account connection User (FE) connector-hub social-bridge (Ctrade adapter) Ctrader OAuth link 1. credentials 2. issue address 3. OAuth login 4. social-bridge receives authorized accounts list via OAuth success → returns list to user 5. User selects the account to mount connector-hub social-bridge 6. child address + connect req 🐇 sessions queue 7a. { connector_connected: false, synced: false } 🐇 sessions queue 7b. { connector_connected: true, synced: false } 🐇 sessions queue 7c. { connector_connected: true, synced: true } connector-hub temp-trader ACT account on true / true 8. request ACT account 9. pull symbols → symbol mapping leader-service → FTL if Leader

How it works

1. Credentials — User submits credentials from FE. connector-hub issues an address and forwards the request to social-bridge (Ctrade adapter).

2. OAuth login — social-bridge redirects the user through the Ctrader OAuth link. User authorizes access at Ctrader.

3. Authorized accounts list — On successful OAuth, social-bridge receives the list of authorized Ctrader accounts and returns it to the user.

4. User selects account — User picks which Ctrader account to mount.

5. Child address + connection requestconnector-hub issues a child address for the selected account and sends the account connection request to social-bridge.

6. Sessions events (3 states) — social-bridge publishes three events on the sessions queue: {false,false}{true,false}{true,true}.

7. Mirror ACT account — On the final {connector_connected:true, synced:true} event, connector-hub requests a mirror ACT account from temp-trader. Symbols are then pulled and symbol mapping runs.

8. Role-specific step — If Leader, leader-service sets up the Leader profile on FTL. If Copier, copier-service wires the copy relationship. Ctrade is available for both roles.

ACT · account connection User (FE) connector-hub act-bridge credentials issue address + connect req ACT broker session open session session ready connector-hub temp-trader ACT mirror account pull symbols → symbol mapping leader-service → FTL if Leader

How it works

1. Credentials — User submits ACT credentials. connector-hub issues an address and sends a connection request to act-bridge.

2. Open ACT broker sessionact-bridge opens the ACT broker session directly (no sessions-queue hand-off for ACT). On success, the session is handed back to connector-hub.

3. Mirror ACT account — connector-hub requests a mirror ACT account via temp-trader. Symbols are pulled and symbol mapping runs.

4. Role-specific step — If Leader, leader-service sets up the Leader profile on FTL. If Copier, copier-service wires the copy relationship.

Note: ACT is available for both Leader and Copier roles.

Telegram · account connection (Leader only) User (FE) connector-hub social-bridge (Telegram adapter) TG handle + token issue address + connect req 🐇 sessions queue { connector_connected: false, synced: false } TG bot / channel listener verify + attach 🐇 sessions queue { connector_connected: true, synced: true } connector-hub temp-trader ACT mirror account leader-service → FTL

How it works

Leader-only path. Telegram is not available as a Copier source — copying flows still use MT/ACT/Ctrade for execution.

1. Connect handle — User provides their Telegram handle / bot token. connector-hub issues an address and forwards to social-bridge (Telegram adapter).

2. Sessions state — social-bridge publishes { connector_connected: false, synced: false }, attaches a TG bot / channel listener, then publishes { connector_connected: true, synced: true }.

3. Mirror ACT + Leader profile — temp-trader creates the mirror ACT account (so copiers can execute real trades off the Leader's TG signals); leader-service sets up the Leader profile on FTL.

Discord · account connection (Leader only) User (FE) connector-hub social-bridge (Discord adapter) Discord user + server issue address + connect req 🐇 sessions queue { connector_connected: false, synced: false } Discord channel / webhook verify + attach 🐇 sessions queue { connector_connected: true, synced: true } connector-hub temp-trader ACT mirror account leader-service → FTL

How it works

Leader-only path. Discord (like Telegram) is not available for Copiers — their execution still uses MT/ACT/Ctrade.

1. Connect server — User provides their Discord user + server / channel. connector-hub forwards to social-bridge (Discord adapter).

2. Sessions state — social-bridge publishes { connector_connected: false, synced: false }, attaches to the Discord channel / webhook, then publishes { connector_connected: true, synced: true }.

3. Mirror ACT + Leader profile — temp-trader creates the mirror ACT account; leader-service sets up the Leader profile on FTL.

Services

connector-hub
Central orchestrator. Issues the unique account ID, routes the request to the right bridge, tracks session state from the sessions queue, and triggers the mirror-account + Leader/Copier wiring.
social-bridge (node-middleware)
Mounts MT4/MT5 (K8s pod), Ctrade, Telegram, and Discord connections. Publishes state transitions on the sessions queue.
act-bridge
Mounts ACT broker sessions. Handles ACT-specific authentication and symbol discovery.
temp-trader
Creates the mirror ACT customer account for every connection (even non-ACT sources) so that trades can be mirrored. Emits ActCustomer events.
leader-service
Sets up the Leader profile on FTL after a successful connection — so the user's orders are broadcast as signals.
copier-service
Wires the copy relationship for a Copier — allocation, risk caps, copy-start / copy-stop logic. Not used for Telegram/Discord sources.
users-service
Consumes ActCustomer events and stores the details in the Postgres user profile.

Role availability

  • Leader → MT4, MT5, ACT, Ctrade, Telegram, Discord.
  • Copier → MT4, MT5, ACT, Ctrade only (social sources can't act as copy execution venues).

Architecture improvements

Target-state direction for the account-connection layer. Built service-by-service against code citations — no assumed behaviour. Each tab groups the proposal a different way: the decisions we're committing to, the principles that back them, the concrete risks each decision resolves, and the APIs touched. Starts with connector-hub; more services will be added as they're individually reviewed.

Decompose → connector-hub's inbound RMQ surface
Document all 10 consumer queues in the module diagram, one plugin owns each: sessions (queue-listners/sessions.ts:528), act_customer / act_mounting / act_balance (act/handler-remote/index.ts:140-152), subscription-cancel (copy-trading/handler-remote/index.ts:71-73), ftl_unregister, SymbolMapping, info-balance, signal.verify, error (queue-listners/*.ts). Today they're discovered by grep, not by reading the doc.
Externalize the RMQ broker registry
connector-hub loads its broker list from the opaque RABBIT_INIT JSON env (libs/rabbitmq/config/config.ts:7). Replace with an explicit, version-controlled registry so the broker topology is auditable without reading deployment env vars.
Document the hidden external integrations
Promote FXView (POST https://conversion.fxview.com/api/get/pricequeue-listners/info.ts:106-107), CP-Core (CP_CORE_BASEPATHlibs/cp-core-helper/config/config.ts:5), and node-middleware (MIDDLEWARE_BASE_URLbrokers/config/config.ts:5) to first-class integrations in the Module 02 External-integrations pills. Today they live only as env vars buried in plugin configs.
Upgrade → temp-trader (NestJS + RabbitMQ)
Today temp-trader is HTTP + MySQL only — zero RabbitMQ code in the repo (grep confirmed). Rebuild on NestJS with RMQ consumer + publisher so it owns ACT customer + demo account creation end-to-end and publishes ACTCustomer itself. Decision already taken in Onboarding improvements; listed here for the account-connection context (every broker-mount flow routes through this service for the mirror ACT account).
Consolidate → participants-service (leaders + copier)
leaders-service and copier-service are ~95% byte-identical and already drifting (PUT helper exists only in copier-service). Collapse into one participants-service with plugin-per-role. Full plan in Leader + Copier Consolidation. Called out here because every account-connection flow (MT4/MT5/Ctrade/ACT/TG/Discord) hits one of these two services for the Leader or Copier record.
Close the leader-update gap
copier-service exposes PUT /following/update (plugins/followers/routes/index.ts:37); leaders-service has no equivalent — leader profiles cannot be updated via the service today. The consolidated generic base exposes PUT /participants/:role/:uuid for every role by default.
Real service-to-service auth to FTL
Both leaders-service and copier-service send "temp" as a hardcoded auth header on every FTL call. Replace with an env-injected service token, validated on the FTL side, injected via one http-client interceptor rather than per-route.
Validators mandatory at the edge
The shared/validator/index.ts schemas in leaders-service and copier-service are empty — unvalidated payloads are forwarded straight to FTL. Validator becomes a required constructor arg of ParticipantRoute; fail-fast at the edge.
Idempotent ACTCustomer consumer in users-service
users-service consumes ACTCustomer on queue usersService (handler-remote/index.ts:25, 65) and writes ACT details back to the user row. No eventId dedup today — a redelivery will re-write the same row. Add the Redis-backed dedup helper (cross-ref Onboarding ADR-06).
Event-driven sessions state (node-middleware)
The sessions queue today uses sendToQueue with no exchange (core/rabbitmq.js:33-35, 128-130) and carries a boolean pair (connector_connected, synced) — publish sites at actions/copy-trading-core/v1/login.js:212-219, core/websocket.js:135-140, actions/connectors/v1/profile.js:47-52, connectors/ctrade.js:707-708. Move to a named topic exchange with explicit state-transition events so consumers can subscribe selectively without editing the publisher.
Promote AI 2FA extraction to first-class (node-middleware)
core/ai.js:27-268 consumes AI_MESSAGE, calls OpenAI (chat.completions.create at :63), regex-extracts a 64-char verification token (:36-39), posts it to RABBITMQ_SIGNAL_VERIFICATION_QUEUE via actions/connectors/v1/verification.js:20-29. MT / cTrader mounts can't complete without this loop — promote to a first-class integration pill with a service card, not buried in an internal queue.
Distributed lock on ACT token refresh (act-bridge)
generateAuthenticationToken() at act-bridge.service.ts:735-806 fetches a session token from /auth/token2 and caches it in Redis key ACTBRIDGE:{platform}:{actUsername} with a 3600s TTL (:782-783). No distributed lock — two concurrent requests at TTL expiry can both call the ACT broker. Wrap with Redis SETNX so the token fetch is single-flight per (platform, account).
Notify on mount failure (act-bridge)
Token-fetch failures throw at act-bridge.service.ts:793-805 but no user notification fires. The Notify.toUser(...).module('GENERIC').type('ERR_ORDER') pattern used on order errors (:299, 309, 440) should extend to the mount / token-fetch failure path so users see a clear "ACT broker connection failed" instead of a silent retry loop.
P1 Every RMQ queue has a documented owning plugin
Inbound surface is declared in the diagram, not discovered by grep. Each of connector-hub's 10 queues belongs to exactly one plugin (act, copy-trading, symbol-mapping, queue-listners, …) — no cross-plugin consumer routing.
P2 External HTTP dependencies are first-class in the doc
If the service calls an external host over HTTP (FTL, ActTrader, FXView, CP-Core, node-middleware), it appears as an external-integration pill and in the service card — not only as an env var in the code.
P3 Publishers own their events — no ghost publishers
If the doc says "service X publishes event Y", service X must have the publish() call in its repo. No more cases like temp-trader being documented as the ACTCustomer publisher while the actual publish() lives in connector-hub. Contract tests on both sides.
P4 Participant gateways are thin — FTL is the source of truth
leaders-service and copier-service (or the consolidated participants-service) own no business state. Their job is validate + translate + forward. FTL is authoritative for Leader / Copier records; any local state is transient cache.
P5 Validators mandatory at every route
Empty-validator routes forwarding unvalidated payloads to FTL is an anti-pattern — rejected in code review. A request that can't succeed downstream is rejected locally at the route handler.
P6 Idempotent event consumers
Every consumer of an RMQ event (starting with ACTCustomer in users-service) dedups on eventId via Redis (24 h TTL). Redeliveries and duplicate publishes are safe by construction.
P7 Named exchanges over direct queues
Every publisher uses an exchange + routing key, not sendToQueue. Adding a new consumer becomes a bind-only change — no edit to the publisher. The sessions queue (node-middleware/core/rabbitmq.js:33-35) is the immediate fix target.
P8 Single-flight on external token fetch
Any broker-session token refresh (act-bridge ACT tokens, cTrader OAuth, others) is gated by a distributed lock keyed on (platform, account). Two concurrent requests at TTL expiry resolve to one external fetch.
IDRisk todayResolved by
R1connector-hub has 10 RMQ consumers that don't appear in the Module 02 diagram or service cards — debugging a missing event starts with grep across plugins/queue-listners/ and plugins/act/handler-remote.Document every queue + binding in the diagram; assign an owning plugin with a dashboard per plugin.
R2The HTML documented subscription-end as the connector-hub consumer topic; code actually binds subscription-cancelcancelSubs (copy-trading/handler-remote/index.ts:71-73). Doc / code drift.Fix the HTML label; add a JSON-schema contract test so this kind of drift fails in CI.
R3FXView (https://conversion.fxview.com), CP-Core, and node-middleware are called by connector-hub but not listed in the Module 02 External-integrations pills — a reader can't discover them without reading plugins/brokers/config, libs/cp-core-helper, and queue-listners/info.ts.Promote all three to first-class external integrations in the module header + service card.
R4temp-trader has zero RabbitMQ code (grep confirmed) yet the HTML documented it as the ACTCustomer publisher. connector-hub silently took over that role — doc / code drift. On-call debugging starts in the wrong service.Upgrade temp-trader to NestJS + RMQ. It owns the publish; connector-hub stops publishing on its behalf. Contract test on both sides.
R5leaders-service has no update endpoint — leader profiles can only be created or deleted. copier-service has PUT /following/update (plugins/followers/routes/index.ts:37). Asymmetric drift between two services that are supposed to be siblings.Consolidated participants-service generic base exposes PUT /participants/:role/:uuid for every role by default.
R6The shared/validator/index.ts schemas in both leaders-service and copier-service are empty. Client payloads are forwarded to FTL without validation — bad inputs fail deep in the stack, not at the edge.Validator is a required constructor arg of every ParticipantRoute; fail-fast at the route handler.
R7Hardcoded string "temp" is sent as the auth header on every FTL call from leaders-service and copier-service. Not a secret, not rotatable, not auditable.Env-injected service token, validated on the FTL side; one http-client interceptor injects for all routes.
R8leaders-service and copier-service are ~95% byte-identical. Fixes already drift (PUT helper exists only in copier-service). Twice the CI, twice the deploys, twice the dashboards for near-identical logic.Collapse into zulu3.0-participants-service with plugin-per-role. See Leader + Copier Consolidation.
R9users-service's ACTCustomer consumer (handler-remote/index.ts:25, 65) has no idempotency. A redelivery re-writes the same user row; duplicate publishes do the same.Redis-backed SET NX PX 86400000 dedup on eventId. Shared helper, used by every consumer. See Onboarding ADR-06.
R10The sessions queue publisher (node-middleware/core/rabbitmq.js:33-35, 128-130) uses direct sendToQueue with no exchange. Every new consumer requires a publisher edit; the topology is invisible to the broker dashboard.Move to a named topic exchange + routing keys per state transition. Consumers bind; publisher stays untouched.
R11MT / cTrader mounts depend on the AI-based 2FA extraction loop at node-middleware/core/ai.js:27-268 — OpenAI call at :63. The dependency is invisible in the Module 02 diagram; a silent failure there stalls every affected mount.Promote OpenAI (and the AI_MESSAGE / SIGNAL_VERIFICATION queue pair) to a first-class external integration + service card. Alert on consumer lag.
R12Token-fetch race: two requests hitting act-bridge.service.ts:691-705 at TTL expiry both call /auth/token2. No distributed lock.Redis SETNX around generateAuthenticationToken(); single-flight per (platform, account).
R13Mount / token failures in act-bridge throw at act-bridge.service.ts:793-805 without firing a user notification. Only order failures hit Notify.toUser() (:299, 309, 440).Extend the existing Notify.toUser(...) pattern to the token-fetch failure path — users get a clear ACT-connection error instead of silent retries.

Click an endpoint to expand its request & response. Fields shown are those cited in the per-service API docs; where a body is not fully inferrable the handler's delegate pattern is noted. Full API reference lives in api_docs/connector-hub.md, api_docs/temp-trader.md, api_docs/leaders-service.md, api_docs/copier-service.md.

zulu3.0-connector-hub · central orchestrator — broker mounts + copy-trading setup

POST /mount/act connector-hub · act/routes/index.ts:52
Enum: ACT_LOGIN Validator: ACTValidators.actLogin() Upstream: ACT login via temp-trader (ACT_TRADER_HOST)
Request · body
{
  "account_id": "string",
  "act_password": "string",
  "broker_name": "string?",
  "broker_id": "string?",
  "server_name": "string?",
  "server_id": "string?",
  "account_type": "…",              // validated by ACTValidators.actLogin()
  "account_privacy": "…"
}
Response
Shape not fully documented in the route file — delegates to ACT login flow.
PUT /mount/act/reconnect connector-hub · act/routes/index.ts:53
Enum: ACT_RECONNECT Purpose: re-mount an already-registered ACT account after credential change
Request · body
{
  "algogems_account_address": "string",
  "act_password": "string",
  "broker_name": "string?",
  "server_name": "string?"
}
POST /v2/create/demo connector-hub · act/routes/index.ts:55
Enum: CREATE_DEMO_ACCOUNT_V2 Validator: ACTValidators.createDemoAccountV2() Upstream: ACT_TRADER_HOST — create-customer + trader + account; seeds ACT_DEMO_INITIAL_BALANCE; enforces ACT_DEMO_ACCOUNT_LIMIT Quirk: rejects emails containing "nodm" with QA_TEST_CASE 400
Request · body
{
  "username": "string?",
  "email": "from auth"
  // additional fields per ACTValidators.createDemoAccountV2()
}
POST /leader/create connector-hub · copy-trading/routes/index.ts:28
Enum: CREATE_LEADER Auth: AuthUtil Upstream: POST ${LEADERS_SERVICE_URL}/ftl/leader/create (handler-remote/index.ts:411) — leaders-service → FTL
Request · body
{
  "algogems_account_address": "string",
  "leader_display_name": "string",
  "leader_description": "string"
}
Response · 201
{ "Data": "<leader record>" }
POST /copy connector-hub · copy-trading/routes/index.ts:59
Enum: COPY_LEADER Validator: CopyTradingValidators.copyLeader() Auth: AuthUtil Upstream: POST ${COPIER_SERVICE_URL}/ftl/following/create (handler-remote/index.ts:1424) — copier-service → FTL RMQ: publishes startCopy (handler-remote/index.ts:1470)
Request · body
{
  "leader_uuid": "string",
  "copier_settings": { /* … */ },
  "algogems_account_address": "string"
  // full shape not inferrable — handler delegates req.body
}
Response · 201
{ "Data": "<follower record>" }
DELETE /copy/stop connector-hub · copy-trading/routes/index.ts:61
Enum: STOP_COPY Upstream: DELETE ${COPIER_SERVICE_URL}/ftl/following/remove?uuid=… (handler-remote/index.ts:1652) RMQ: publishes stopCopy (handler-remote/index.ts:1687)
Request · query
?leader_uuid=<uuid>&algogems_account_address=<addr>&closeAccount=<bool>
GET /v2/platforms connector-hub · brokers/routes/index.ts:67
Enum: GET_ACTIVE_PLATFORMS_V2 Purpose: platforms available to the current user's role
Response
[
  {
    "display_name": "string",
    "platform_code": "MT4" | "MT5" | "ACT" | "CTRADE" | "Telegram" | "Discord",
    "is_for_leader": boolean,
    "is_for_copier": boolean,
    "is_for_signal_provider": boolean,
    "active": boolean,
    "flow_code": "FORM" | "OTHERS"
  }
]

zulu3.0-temp-trader · ACT customer + account provisioning (NestJS + RMQ target)

POST /actTrader/customer/create temp-trader · actTrader/routes/index.js
Caller: connector-hub on userRegister fan-out Downstream: ActTrader HTTP API Target state: triggered by RMQ event create.act.customer; response becomes ACTCustomer publish
Request · body
Shape not fully documented in the route file (api_docs/temp-trader.md flags this). The current HTTP caller (connector-hub) forwards user + profile fields.
POST /actTrader/account/create temp-trader · actTrader/routes/index.js
Caller: connector-hub during demo + live ACT mount flows Downstream: ActTrader client-portal API (CLIENT_PORTAL_API_HOST_ACT_{LIVE,DEMO})
Request · body
Shape not fully documented in the route file. The account-create flow binds to the ACT customer id created by /actTrader/customer/create.
POST /actTrader/modifyaccount temp-trader · actTrader/routes/index.js
Caller: connector-hub on balance / leverage / password change
Request · body
Shape not fully documented in the route file.

social-trader3.0-node-middleware · broker mount bridge — MT4/MT5, cTrader, Telegram, Discord

POST /login node-middleware · actions/copy-trading-core/v1/login.js:8
Caller: connector-hub on every mount flow (MT4/MT5/Ctrade/Telegram/Discord/Binance) Per-platform logic: MT → K8s pod launch (connectors/metatrader.js:117-167, 248-252); cTrader → OAuth + axios (connectors/ctrade.js:10-44); Telegram → @mtproto/core (connectors/telegram.js:1, 580-603); Discord → discord.js (connectors/discord.js:3-19) Initial state: connector_connected=false for MT / cTrader (async WS handshake); true for Telegram / Discord / Binance (login.js:212-219)
Request · body
Platform-specific. Payload shape differs per platform DTO — fields documented in per-connector files in connectors/.
Response
{ "session_id": "…", "account_data_id": "…" }
POST /profile node-middleware · actions/connectors/v1/profile.js:6
Purpose: completes mount after MT / cTrader account profile sync Effect: sets connector_connected=true AND synced=true on the session, publishes to the sessions queue (profile.js:47-53) Writes: account_profile table (data, orders, positions) at profile.js:27-42

Async handoffs on the account-connection path · not HTTP — RabbitMQ events

EVENT 🐇 ACTCustomer (consumer) users-service · handler-remote/index.ts:25, 65
Consumer: users-service — queue usersService, topic ACTCustomer Publisher (today): connector-hub (drift; temp-trader has no RMQ) Publisher (target): temp-trader after NestJS + RMQ upgrade Idempotency: none today — see R9
Effect
On receipt, users-service writes ACT details (act_account_id, act_platform, act_username) back to the user row in Postgres.
EVENT 🐇 startCopy · stopCopy connector-hub → rewards-service · subscriptions
Publisher: connector-hubcopy-trading/handler-remote/index.ts:1470 (startCopy), :1687 (stopCopy) Consumers: rewards-service (session lifecycle), subscriptions (entitlement check)
Trigger
POST /copy → startCopy publish · DELETE /copy/stop → stopCopy publish
Payload
Payload field set not inferrable from the route file — handler delegates req.body. See `api_docs/rewards-service.md` for the consumer's expected envelope.
EVENT 🐇 sessions (state transitions) node-middleware → connector-hub
Publisher: node-middleware — direct sendToQueue (no exchange), queue name from RABBITMQ_SESSIONS_QUEUE env Publish sites: actions/copy-trading-core/v1/login.js:212-219, core/websocket.js:135-140, actions/connectors/v1/profile.js:47-52, connectors/ctrade.js:707-708 Consumer: connector-hub — queue sessions (queue-listners/sessions.ts:528-529) Topology gap: no exchange — every new consumer needs a publisher edit. See R10.
Payload
{
  "connector_connected": boolean,    // false for MT / cTrader on login; true after WebSocket handshake
  "synced": boolean,                 // true after profile sync completes
  // account_data metadata + session fields
}
EVENT 🐇 AI_MESSAGE → SIGNAL_VERIFICATION (2FA loop) node-middleware internal
Producers: actions/connectors/v1/telegram-signal.js:26, discord-signal.js:28 Consumer: core/ai.js:27-268 — calls OpenAI chat.completions.create at :63; regex-extracts 64-char token at :36-39 Downstream: extracted token posted to RABBITMQ_SIGNAL_VERIFICATION_QUEUE via actions/connectors/v1/verification.js:20-29 Criticality: MT / cTrader mounts can't complete without this loop when the broker issues a 2FA challenge. See R11.
Payload
Raw inbound text message from Telegram / Discord; shape varies by platform adapter.
↑ Back to overview
Module 03 · Lifecycle

Copier Flow

How a user becomes a Copier — signup through subscription gating, leader selection, and start-copying activation, then the symmetrical closeout. This module is a narrative that leans on the other modules for detail; it doesn't re-describe services that already have a home elsewhere.

Not in this module: signal ingestion, trade execution, bridges, fills, persistence, analytics. Those live in Modules 05 (Trading Flow), 06 (Bridges), 07 (Analytics). See the Trading Flow module for what happens once a Copier is active.

🔗 External integrations
Stripe FTL S281 (demo)
Copier Flow · signup → active copy session 1. Account setup auth · users 2. Broker connection connector-hub · temp-trader 3. Subscription gating subscriptions · Stripe entitlement + eligibility 4. Leader discovery leaders-service → FTL 5. Start copying connector-hub → copier-service (create follower on FTL) → publish startCopy on RabbitMQ · rewards-service creates copy session → notifications-service sends confirmation Copier is active Trading Flow takes over → 6. Stop copying (closeout) connector-hub → copier-service remove · publish stopCopy · rewards-service closes session user action

Phase 1 · Account setup

User signs up or logs in. Detail lives in Module 01 Onboarding & Login — SSO flows (Apple / Google), OTP verification, profile writes by users-service, session issuance by auth-service. Not duplicated here.

Phase 2 · Broker connection

User connects a trading account. Detail lives in Module 02 Account Connection. The orchestrator is connector-hub; demo accounts route through temp-trader to S281. Live accounts go through the MT4 / MT5 / ACT / cTrader connectors.

Phase 3 · Subscription gating

The commercial layer. Copy-trading is gated by a subscription — free or paid.

  • Paid pathsubscriptions exposes plan / cart / payment endpoints. The user checks out through Stripe; a Stripe webhook triggers activation; the subscription is marked active and feature entitlements are written.
  • Free path — a background consumer inside subscriptions processes free-plan activation events. Passing eligibility → auto-activation; failing → user falls back to a paid plan.
  • Credits are allocated per plan; audit history is recorded; state lives in PostgreSQL.

Without an active subscription with the right entitlement, the Start Copying step (below) is blocked.

Phase 4 · Leader discovery / selection

The UI lists available Leaders. client-express fronts the requests; Leader records are fetched via leaders-service, which forwards to FTL. leaders-service owns no data — it's a thin HTTP gateway with logging / validation.

Phase 5 · Start copying

The actual activation. connector-hub orchestrates:

  1. Calls copier-service to create the follower record — forwarded to FTL.
  2. Publishes a startCopy event on RabbitMQ (Social Trader broker, vhost /zulu, exchange zulu).
  3. rewards-service consumes startCopy, creates a copy session in PostgreSQL, and schedules recurring reward jobs via BullMQ. Detail in Module 08 Rewards.
  4. notifications-service sends a "you're now copying X" confirmation via the user's preferred channel.

From this point on, the Copier is active. Signals from the Leader's trades flow through Module 05 Trading Flow and Module 06 Bridges — not this module.

Phase 6 · Stop copying (closeout)

  1. User stops from UI → connector-hubcopier-service removes the follower record on FTL.
  2. connector-hub publishes stopCopy on RabbitMQ.
  3. rewards-service closes the copy session. Background workers continue to process pending reward transactions, handle overdue / recovery, and run month-end settlement so earned rewards aren't lost even if the service was briefly down.
  4. The subscription itself continues unless the user cancels / pauses separately via subscriptions APIs.
  5. If the subscription ends, copying also stopssubscriptions publishes the subscription-end event; connector-hub reacts by triggering stopCopy for the affected follower records.

Services touched in this lifecycle

ServiceRole in Copier FlowPrimary module
connector-hubOrchestrator (calls copier-service, publishes startCopy/stopCopy)02
copier-serviceFollower CRUD → FTL02
subscriptionsPlan / cart / activation / renewal (Stripe)inline here · breadcrumb in 10
rewards-serviceCopy-session bootstrap on startCopy; session close on stopCopy08
notifications-serviceUser confirmations10
🧭 Where the copy flow picks up next
Once the Copier is active, every trade the Leader places fans out through the signal pipeline to this Copier's execution venue. That's a different module — see Module 05 Trading Flow for the real-time pipeline and Module 06 Bridges for how orders reach the actual broker.
↑ Back to overview
Module 04 · Lifecycle

Leader Flow

How a user becomes a Leader — signup through broker connection, Leader application, admin approval, registration on FTL, and strategy setup. Unlike Copier Flow, Leader registration has a human in the loop: an admin reviews and approves each new Leader.

Not in this module: the trade signal publishing once a Leader is active, bridges, analytics. Leader trades flow through Module 05 Trading Flow.

🔗 External integrations
FTL
👑 Admin-gated
Unlike Copier registration (self-serve), Leader registration requires explicit admin approval via admin-express + admin-service. No subscription is required — Leader participation is free-to-register and earning-based.
Leader Flow · application → admin approval → active 1. Account setup auth · users 2. Broker connection connector-hub · temp-trader 3. Leader application connector-hub 4. Admin approval admin-express · admin-service human in the loop rejected flow ends 5. Leader registration + strategy setup connector-hub → leaders-service (create) → FTL → strategy / watchlist configured via connector-hub → notifications-service informs the user Leader is active Trades → Trading Flow · earnings → Rewards 6. Closeout self-serve: user deregisters · or admin revokes via admin-express → connector-hub → leaders-service (remove) approved user or admin

Phase 1 · Account setup

User signs up or logs in. Detail in Module 01 Onboarding & Login — SSO, OTP, profile writes. Not duplicated here.

Phase 2 · Broker connection

User connects the broker whose trades they want to publish. Detail in Module 02 Account Connection. Orchestrated by connector-hub; ACT-side connections go through temp-trader. Demo accounts aren't relevant here — Leaders need a live broker.

Phase 3 · Leader application

The user submits a Leader application from the client UI via connector-hub. The application creates a pending Leader record waiting for admin review.

Phase 4 · Admin approval

An admin reviews the application via admin-express. admin-service handles roles / permissions / approval audit on the admin side. The admin either approves or rejects:

  • Rejected — the flow ends. The user is notified; no Leader record is created on FTL.
  • Approved — the approval action triggers Phase 5. The audit record is retained in admin-service.

Phase 5 · Leader registration + strategy setup

  1. On approval, connector-hub calls leaders-service to create the Leader record. leaders-service owns no state — it forwards to FTL, which holds the actual Leader record.
  2. connector-hub handles leader / watchlist / strategy management (symbols, marketing description, copy settings — exact scope depends on how "strategy" is configured in the platform).
  3. notifications-service sends a "you're now a Leader" confirmation.

Registration is synchronous — no RabbitMQ lifecycle events on the Leader side (unlike startCopy / stopCopy for Copiers).

Phase 6 · Active leader and closeout

Once registered, the Leader's trades become the source of signals. That flow lives in Module 05 Trading Flow. Earnings from copier activity accrue through Module 08 Rewards.

Closeout is two-path:

  • Self-serve — user deregisters from the client UI → connector-hubleaders-service (remove) → FTL.
  • Admin-revoked — admin uses admin-express to revoke the Leader → same downstream path.

Services touched in this lifecycle

ServiceRole in Leader FlowPrimary module
client-expressApplication submission from user UI01
auth-serviceSession + SSO01
users-serviceProfile01
connector-hubOrchestrator (calls leaders-service, owns strategy management)02
leaders-serviceLeader CRUD → FTL02
temp-traderACT-side broker integration02
admin-expressAdmin review + approval gateway11
admin-serviceRoles / permissions / approval audit11
rewards-serviceAccrues Leader earnings from copier activity08
notifications-serviceApproval / rejection / removal notifications10
🧭 Where the leader picks up from here
Once active, every trade the Leader places is a signal in the copy-trading pipeline. See Module 05 Trading Flow for how that signal fans out to Copiers, and Module 08 Rewards for how Leader earnings accumulate.
↑ Back to overview
Module 04B · Gap Analysis & Proposal

Leader + Copier Service Consolidation

A bridge between Module 03 Copier Flow and Module 04 Leader Flow. Both flows terminate at two sibling services — copier-service and leaders-service — that are byte-for-byte identical in ~95% of their code. This module audits the gap and proposes a single participants-service that both flows can call.

Why it lives here: the two services are invoked by both lifecycle flows (Copier registration, Leader registration, and the mirroring closeout paths). Consolidating them affects every phase of Modules 03 and 04 that says "connector-hub → leaders-service / copier-service".

Architecture improvements — Leader + Copier Service Consolidation

Target-state direction for the two sibling services that back Module 03 Copier Flow and Module 04 Leader Flow. Today copier-service and leaders-service are ~95% byte-identical and already drifting; the proposal folds both into a single zulu3.0-participants-service with per-role plugins. Each tab groups the proposal a different way: the decisions we're committing to, the principles that back them, and the concrete risks each decision resolves.

Merge → zulu3.0-participants-service
Collapse copier-service and leaders-service into one service. 95% duplication with active drift (ClientFactory.put() only in copier-service) is the decider.
Plugin-per-role, not if/else
plugins/leaders/ and plugins/copiers/ stay as separate plugin dirs so adding future roles (institutional, auto-copier) is mechanical — no new controller class, no new repo.
Role-agnostic base
Generic ParticipantRoute / ParticipantRemote in _participants-base/; per-role files only declare config + schema. Update/create/list/remove are inherited, not copy-pasted.
FTL stays the source of truth
The service remains a gateway — no business rules duplicated from FTL. handler-remote is the only side that talks to FTL; handler-local is this service's own state only.
RabbitMQ becomes mandatory
Stop shipping a dead dependency. Every state change emits participant.leader.* / participant.copier.* so signal-junction, order-update, persistence-service can react.
Single deploy unit
One image, one Dockerfile, one alert set, one dashboard. Scale by replica count — route-level scaling wasn't being used anyway (same process, same DB in both services today).
Close the leader-update gap
Copiers have PUT today; leaders have no update endpoint at all. Generic base exposes PUT /participants/:role/:uuid for every role by default.
Real service-to-service auth
Replace the hardcoded "temp" token (used on every FTL call in both services today) with an env-injected service token validated on the FTL side.
Validators at the edge
Current shared/validator/index.ts schemas are empty — unvalidated payloads forwarded to FTL. Validator becomes a required constructor arg of ParticipantRoute.
Thin gateway, fat events
Minimal logic inline; rich event payloads for downstream services. If a consumer needs more context, enrich the event — don't spawn new HTTP calls.
Contract symmetry
Same verbs, shapes, error envelope for every participant role. Leader create and Copier create use the same body structure (today one uses query params, the other uses JSON body).
Fail-fast at the edge
Validate before the FTL hop; never forward unvalidated payloads. Every request is rejected locally if it can't succeed downstream.
Config over code
New role = add a plugin config (FTL paths, event topics, model, validator). No new controller class, no new route wiring.
Explicit boundaries
handler-local for this service's own state, handler-remote for FTL — never mix. Prevents the gateway from silently becoming a business-logic service.
Observability by default
Correlation-id in, correlation-id out, on every HTTP and MQ hop. Axios interceptor forwards x-request-id to FTL; event envelope carries it to consumers.
Risk todayHow the unified service resolves it
Infra drift across two repos (PUT helper, logger tweaks diverging)Single core/, libs/, middlewares/ — one place to change
Inconsistent FTL contract (body vs query)ParticipantRemote enforces body-only payloads uniformly
Missing leader-update capabilityGeneric base exposes update for every role by default
Hardcoded "temp" auth tokenOne http-client interceptor injects real service token for all roles
Unused RabbitMQ → downstream services starve for eventsParticipantEvents publisher is in the hot path, can't be skipped
Double deploy / monitoring overhead for trivial logicOne image, one alert set, one dashboard
Future role (e.g. institutional copier) = new repoNew role = new plugin dir + config file
Empty validators = unvalidated proxyValidator is a required constructor arg of ParticipantRoute
Correlation-id lost at the FTL hopAxios interceptor forwards x-request-id; event envelope carries it to consumers
SSL certs committed to repo (cert.pem / key.pem)Mounted from secrets, excluded from repo

Click an endpoint to expand its request & response. Responses follow the shared envelope { Data: <payload> } on success or { HttpCode, HttpMessage, ErrorCode, Message } on error (see @core/modules/http-response-handler).

zulu3.0-copier-service

POST /following/create copier-service · followers/routes/index.ts
Handler: _createFollower Forwards to FTL: POST {FTL_BASE_URL}/follower/register Auth header: "temp" (hardcoded)
Request · body
{
  "platform": "MT4",              // string
  "account": 123456,              // number
  "algogems_account_address": "0xabc..."   // string
}
Response · 200
{
  "Data": { /* FTL response body from /follower/register */ }
}
Response · 500
{
  "HttpCode": 500,
  "HttpMessage": "INTERNAL SERVER ERROR",
  "ErrorCode": "INTERNAL_SERVER_ERROR",
  "Message": "<error.message>"
}
GET /followings copier-service · followers/routes/index.ts
Handler: _getFollowers Forwards to FTL: GET {FTL_BASE_URL}/follower/list
Request · query
?active=true        // "true" | "false"
Response · 200
{
  "Data": { /* FTL response — list of followers */ }
}
Response · 500
{
  "HttpCode": 500,
  "HttpMessage": "INTERNAL SERVER ERROR",
  "ErrorCode": "INTERNAL_SERVER_ERROR",
  "Message": "<error.message>"
}
DELETE /following/remove copier-service · followers/routes/index.ts
Handler: _removeFollower Forwards to FTL: DELETE {FTL_BASE_URL}/follower/unregister
Request · query
?platform=MT4&account=123456&uuid=<follower-uuid>
Response · 200
{
  "Data": { /* FTL response from /follower/unregister */ }
}
Response · 500
{
  "HttpCode": 500,
  "HttpMessage": "INTERNAL SERVER ERROR",
  "ErrorCode": "INTERNAL_SERVER_ERROR",
  "Message": "<error.message>"
}
PUT /following/update copier-service · followers/routes/index.ts
Handler: _updateFollower Forwards to FTL: PUT {FTL_BASE_URL}/follower/modify Note: entire req (query + body) is passed through
Request · body + query (both forwarded)
body: {
  "algogems_account_address": "0xabc...",
  /* any update fields accepted by FTL /follower/modify */
}
query: { /* optional filters passed through */ }
Response · 200
{
  "Data": { /* FTL response from /follower/modify */ }
}
Response · 500
{
  "HttpCode": 500,
  "HttpMessage": "INTERNAL SERVER ERROR",
  "ErrorCode": "INTERNAL_SERVER_ERROR",
  "Message": "<error.message>"
}

zulu3.0-leaders-service

POST /leader/create leaders-service · leaders/routes/index.ts
Handler: _createLeader Forwards to FTL: POST {FTL_BASE_URL}/leader/register?account=..&platform=..&brokerage=.. ⚠ Contract quirk: sends empty body, params in URL query
Request · body
{
  "platform": "MT5",                         // string
  "account": 987654,                         // number
  "algogems_account_address": "0xdef...",   // string
  "brokerage": true                          // boolean  (leader-only field)
}
Response · 200
{
  "Data": { /* FTL response body from /leader/register */ }
}
Response · 500
{
  "HttpCode": 500,
  "HttpMessage": "INTERNAL SERVER ERROR",
  "ErrorCode": "INTERNAL_SERVER_ERROR",
  "Message": "<error.message>"
}
GET /leaders leaders-service · leaders/routes/index.ts
Handler: _getLeaders Forwards to FTL: GET {FTL_BASE_URL}/leader/list
Request · query
?active=true        // "true" | "false"
Response · 200
{
  "Data": { /* FTL response — list of leaders */ }
}
Response · 500
{
  "HttpCode": 500,
  "HttpMessage": "INTERNAL SERVER ERROR",
  "ErrorCode": "INTERNAL_SERVER_ERROR",
  "Message": "<error.message>"
}
DELETE /leader/remove leaders-service · leaders/routes/index.ts
Handler: _removeLeader Forwards to FTL: DELETE {FTL_BASE_URL}/leader/unregister
Request · query
?platform=MT5&account=987654&uuid=<leader-uuid>
Response · 200
{
  "Data": { /* FTL response from /leader/unregister */ }
}
Response · 500
{
  "HttpCode": 500,
  "HttpMessage": "INTERNAL SERVER ERROR",
  "ErrorCode": "INTERNAL_SERVER_ERROR",
  "Message": "<error.message>"
}
PUT /leader/update ⚠ missing today — gap
Status: does not exist in leaders-service — asymmetry with copier-service PUT /following/update
Proposed after consolidation
PUT /participants/leaders/:uuid
body: { /* leader fields to update */ }

→ forwarded as PUT {FTL_BASE_URL}/leader/modify
→ emits participant.leader.updated event

Shared (both services)

GET /health @core/modules/heath-check
Purpose: Liveness probe (no downstream check of FTL / MySQL / RabbitMQ today)
Response · 200
{
  "status": "ok"
}

Impact on existing flows

  • Module 03 Copier Flow — Phase "create follower on FTL" and closeout both retarget to /participants/copiers/*. No change to connector-hub orchestration logic, just URL + token.
  • Module 04 Leader Flow — Phase 5 registration and Phase 6 closeout retarget to /participants/leaders/*. Phase 5 also gains the missing PUT for strategy updates (previously had to go through connector-hub-only paths).
  • Module 05 Trading Flow and Module 10 Other Services — new participant.* events become consumable by signal-junction, order-update, persistence-service, notifications-service.
📦 Migration shape
Roughly 2–3 days of structural refactoring (mostly moving files), plus 1–2 days for validators, event wiring, and auth hardening. Zero business-logic change — both flows in Modules 03 and 04 remain narratively identical, only their terminal service changes.
↑ Back to overview
Module 05 · The Hot Path

Trading Flow

The real-time pipeline that carries trade signals from every entry point — MT4/MT5 terminal, Telegram/Discord, Zulu Web (Demo and Act accounts), and ACT broker feeds — through the common Signal Processor and Signal Out services, out to the execution venues (FTL, ACT brokers, MT terminals). This is the performance-critical heart of the platform.

🔗 External integrations
FTL ACT Brokers Node Middleware (Meta)
🧠 Design principle
Trading has four entry paths — MT4/MT5 terminal, Social (Telegram/Discord), Demo web, and Act platform/web — but they all converge on two common services: Signal Processor and Signal Out. MT & Social come in through Node Middleware → Signal junction → Signal Processor. Web orders enter through Act web trader → Signal Out. Broker-originated events (ACT / Fxview fills) enter via Order Update service over WebSocket and get bridged into RabbitMQ. Signal Processor calls FTL (s281) over HTTP for leader/copier execution; FTL pushes the resulting signals back over WebSocket to act-signal-processor, which publishes ftl-signal events consumed by Signal Out. Nothing on the hot path touches Postgres directly — state lives in Redis, every step is published to RabbitMQ, and the Persistence service consumes signal_orders / trade_logs and writes asynchronously to PGSQL. If Redis is lost the cache is rebuilt from the database.
Trading flow · high-level services overview Entry Middleware Junction Signal Processor External / FTL MT4 / MT5 TG / Discord Act / Demo Web requests Node Middleware Order Update service Act web trader System Signal junction Signal Processor FTL s281 Act Demo act-signal-processor Signal Out service 🐇 signal.out 🐇 ACT queue Persistence 🐇 Persistence queue Persistence service PGSQL Redis (shared) RMQ HTTP WS ftl-signal OU<AccountID> 🐇 web-orders consume → SP (Demo/Act copiers) consume → Node Middleware (MT copiers) · execute on platform 🐇 signal_orders · trade_logs 🐇 Save LEGEND solid = HTTP / WebSocket / direct call 🐇 dashed orange = RabbitMQ event dotted = loop / shared-store reference core common service RabbitMQ queue external system data store Web / terminal source Act / Demo
Flow 1 · Meta (MT4 / MT5) Platform MT4 / MT5 Node Middleware Signal junction 🐇 MT4 queue 🐇 MT5 queue Signal Processor FTL s281 signal-synchronizer (ext) Redis Zulu Web Act web trader 🐇 web-orders queue Signal out service act-signal-processor 🐇 signal.out queue 🐇 Persistence queue Persistence service PGSQL HTTP repaired defect (Redis polled) WebSocket 🐇 ftl-signal MT signals 🐇 signal_orders trade_logs 🐇 signal_orders LEGEND solid = HTTP / WebSocket / direct 🐇 dashed orange = RabbitMQ event queues rendered as pill-shaped orange boxes

How it works

Market order from Meta Terminal: Signal junction receives it and forwards to the platform-specific queue. Signal Processor executes the trade on the FTL account. Order updates come back from Act s281 over WebSocket into Signal Out, which acts on them. If the order is from a leader, act-signal-processor receives copier signals from FTL and pushes them to the signal.out RabbitMQ.

SL/TP add or update from Meta: Signal junction forwards to Signal Processor. We only update Redis + DB — SL/TP is not executed on the FTL account.

Close / partial close from Meta Terminal: Signal junction → Signal Processor → executed on the FTL account.

Pending orders (with or without SL/TP): stored and updated only in Redis + DB. Not executed on FTL until Meta fires the pending order — then Signal Processor runs the trade.

Orders from Zulu Web: come via WebSocket to the Act Trader system → pushed to RabbitMQ → consumed by Signal Out. Rest of the flow is the same as Meta orders.

Note: if a leader adds SL/TP, it is not applied to copier accounts.

Flow 2 · Social (Telegram / Discord) Platform TG / Discord Node Middleware Signal junction 🐇 TG_CONNECTOR q 🐇 DISCORD q Signal Processor FTL s281 signal-synchronizer (ext) Redis Zulu Web Act web trader 🐇 web-orders queue Signal out service act-signal-processor 🐇 signal.out q (MT copiers) 🐇 ACT q (Demo/Act copiers) 🐇 Persistence queue Persistence service PGSQL HTTP repaired defect WebSocket 🐇 ftl-signal MT copiers Demo/Act copiers consume → JAN consume back to SP 🐇 signal_orders trade_logs 🐇 signal_orders LEGEND solid = HTTP / WebSocket / direct 🐇 dashed orange = RabbitMQ event copiers dispatch to MT or ACT queue

How it works

For Telegram and Discord, the flow is the same as Meta with one key difference: we do not push leader events to the signal.out queue. Only copier events are pushed from Signal Out. All actions are executed on the FTL account.

Market / pending order from a social platform: Signal junction → Signal Processor → executed on the FTL account.

SL/TP from a social platform: Signal junction → Signal Processor → executed on the FTL account (social platforms aren't directly connected to a trading platform like Meta).

Close / partial close / modify from a social platform: Signal junction → Signal Processor → executed on the FTL account.

Orders from Zulu Web: via WebSocket to Signal Out → RabbitMQ → Signal Processor → FTL. (Some actions are still pending / not working as expected.)

Note: leader SL/TP is not applied to copiers.

Flow 3 · Demo (Demo account from Web) Zulu Web (Demo) Act web trader 🐇 web-orders queue Signal out service 🐇 ACT queue Signal Processor Redis FTL s281 act-signal-processor 🐇 signal.out q Node Middleware 🐇 Persistence queue Persistence service PGSQL Demo (DemoPublisher) consume HTTP WS 🐇 ftl-signal / order updates MT copiers 🐇 Save signal_orders trade_logs 🐇 Save LEGEND solid = HTTP / WebSocket / direct 🐇 dashed orange = RabbitMQ event Demo web orders loop via Signal out → ACT queue → SP

How it works

Demo accounts are not connected to any external trading platform. The demo account lives on the FTL server (s281); all actions happen on the FTL account. Orders are placed only from Zulu Web.

Market / pending order from Zulu Web: via WebSocket to Act web trader → Signal Out. Signal Out publishes to RabbitMQ based on platform + leader/copier logic. For demo, Signal Processor consumes and executes on the FTL account.

Once the FTL account executes, order updates and copier signals (if any) come back. From here the flow is identical to the Meta flow.

SL/TP add / update, close, partial close from Zulu Web: executed on the FTL account and saved to Redis + DB.

Note: leader SL/TP is not applied to copiers.

Flow 4 · Act Platform Platform Act Fxview s245 Zulu Web (Act) Order Update service Act web trader 🐇 Web order rmq 🐇 act rmq Act-bridge-signal-processor 🐇 act order rmq Signal out service Signal Processor FTL s281 Act Demo act-signal-processor 🐇 act signal out rmq Act Bridge Service Meta Flow (if MT4/5) Demo Flow (if demo) 🐇 Persistence queue Persistence service PGSQL WebSocket WebSocket consume consume HTTP WebSocket 🐇 ftl-signal from web / signal for act loop back if MT4/5 if demo 🐇 Save signal_orders trade_logs 🐇 Save signal_orders LEGEND solid = HTTP / WebSocket / direct 🐇 dashed orange = RabbitMQ event Branch ellipses route to Meta/Demo flows

How it works

The Act flow is similar to Meta, with one key difference: Meta uses Node Middleware, Act uses act-bridge. The rest is mostly the same with a few service swaps.

Order placed from Act Terminal: updates first hit Order Update service → pushed to RabbitMQ → consumed by Act-bridge-signal-processor → creates an event for Signal Processor → Signal Processor executes on the FTL account. Order updates come back from Act s281 via WebSocket to Signal Out. If the order is from a leader, copier signals are generated and pushed to signal.out.

SL/TP add / update from Act: Order Update → RabbitMQ → act-bridge processes logic → saves to Redis + DB. Not executed on the FTL account.

Close / partial close from Act Terminal: Order Update → RabbitMQ → act-bridge processes → Signal Processor → executed on the FTL account.

Pending orders (with SL/TP): stored / updated in Redis + DB via act-bridge-signal-processor only. When Act fires the pending order we get a signal and then execute on FTL.

Orders from Zulu Web: via WebSocket to the Act trader system → pushed to RabbitMQ → consumed by Signal Out → pushed again to RabbitMQ → consumed by act-bridge → executed on the connected broker platform (currently Fxview s245). Updates return through Order Update and continue via act-bridge-signal-processor, which processes the logic and saves to the database.

Pipeline Stages

  1. Ingestion — MT4/MT5 and TG/Discord orders enter via Node Middleware. Web orders (Demo & Act accounts) enter via Act web trader System. ACT platform and Fxview s245 broker events enter via Order Update service over WebSocket.
  2. ClassificationSignal junction routes each MT/Social signal to a platform-specific queue (MT4, MT5, TG_CONNECTOR, DISCORD). Defect detection (price=0 for MT4/MT5/CTrade) writes a defect:<algo>_<order> key to Redis for the external signal-synchronizer to repair.
  3. ProcessingSignal Processor (ts-zulu3.0-signal-processor) consumes the platform queues and the shared ACT queue. It calls FTL s281 via HTTP (leader / copier / order URLs) to execute leader-copier fan-out and publishes signal_orders + trade_logs to the persistence pipeline.
  4. Return / Routing — FTL pushes resulting signals over WebSocket to act-signal-processor, which publishes ftl-signal on the ftl queue. Signal Out consumes it and dispatches via platform publishers: MT copiers → routing key out on signal.out queue (consumed by Node Middleware → terminal); Demo/Act/TG/Discord copiers → routing key ACT on the ACT queue (looped back into Signal Processor); ACT broker dispatch → act signal out queue → Act Bridge Service.
  5. Web-order ack path — For orders placed via Zulu Web, Signal Out also publishes an immediate acknowledgement on routing key OU<AccountID> (Order Update ack) and an RPC reply to the caller's reply queue, then ACKs the web-orders message last.
  6. ACT execution feedback — ACT broker dealer sockets push trade / order events to Order Update service over WebSocket. Order Update republishes on the signalData exchange (routing key order). Act-bridge-signal-processor consumes the act.order queue, normalises, and publishes back into Signal Processor via the ACT queue to keep state coherent.
  7. PersistencePersistence service binds the persistence queue to the signal_orders and trade_logs topics published by Signal Processor and Signal Out. It writes to PostgreSQL (signal_processor.signals, signal_processor.trade_logs) asynchronously, keeping the hot path DB-free. Redis holds the live state; if it is lost the cache is rebuilt from Postgres.

Services

signal-junction
Entry gateway for MT/Social signals. Routes each one to a platform-specific RabbitMQ queue (MT4, MT5, TG_CONNECTOR, DISCORD). Bad signals (e.g. price=0) are written to a Redis defect:* key for the external signal-synchronizer to repair.
signal-processor
The central brain. Consumes all platform queues (MT4, MT5, ACT, Telegram, Discord, cTrader, Demo), validates each signal, decides the order type (new / close / pending / SL / TP), keeps live state in Redis to avoid duplicates, and calls FTL over HTTP to execute. Publishes signal_orders and trade_logs for persistence. Invalid or duplicate signals are rejected safely with traceable logs.
act-signal-processor
Keeps WebSocket connections open to the dealer and FTL feeds. Reads live messages and forwards them to RabbitMQ on FTL routing keys — ftl-signal (copy signals), ftl-execution, ftl-order, unregister — so Signal Processor and Signal Out react in real time.
signal-out
The outgoing order gateway. Picks the correct destination queue per platform (ACT, MT4/5, CTRADE, SUBCT, TG, DISCORD, DEMO), processes FTL signal / execution feedback, and handles order state (open / close / modify / cancel / SL / TP) in Redis. Also drives the web-orders flow: validates web requests, converts them to the internal format, forwards for execution, and replies via the response queue.
act-bridge
Adapter between the internal system and each broker's ActTrader platform. Consumes act.signal.out (market, limit, stop, trailing, pending, modify, cancel, close), resolves the broker's session token, and calls that broker's ACT HTTP API to actually execute. Notifies the user on failure.
act-bridge-signal-processor
Inbound bridge for other brokers' ACT updates. Listens on the act.order queue, matches the ACT account to a user, reconciles order / trade IDs, handles opens, closes, pendings, and SL/TP changes, and publishes normalized messages back into Signal Processor's pipeline. Keeps Redis in sync and emits persistence events.
order-update
Lightweight WebSocket-to-RabbitMQ bridge for live order and account events (currently FXVIEW feed). Keeps sockets alive, classifies events, and publishes trade / order events to the order topic on signalData, and deposit/withdraw events to DPT_WDL on rabbit-social.
persistence-service
The central data writer. Consumes the persistence queue bound to signal_orders and trade_logs, applies INSERT / UPDATE / UPSERT / DELETE on the database (with conflict-safe updates and order-ID changes), and records execution history for trade logs. Processes updates per-order-ID to keep newer events from being overwritten by older ones.

Architecture improvements

Four tabs covering the proposed direction for the trading-flow services: Key decisions names what each of the 8 services becomes (merge, shrink, clean, extract, or keep); Design principles states the four rules the new layout is built on (single writer per topic, command / event split, trade_id correlation everywhere, Postgres off the hot path); Risks resolved ties each change to a concrete current-system risk with evidence from code; APIs is an index of the used HTTP endpoints in scope. Full rationale and citations in gap_analysis/architecture_redesign_proposal_2026-04-22.md.

Merge → broker-ingress
order-update + act-signal-processor collapse into one generic WebSocket → RMQ relay (both are DEALER_SOCKET near-clones). New brokers become config, not new services.
Shrink → trading-engine
signal-processor becomes a pure state machine + FTL executor. Platform queues (MT4, MT5, CTRADE, TG_CONNECTOR, DISCORD) stay as-is for back-pressure isolation and independent scaling — each is consumed by a dedicated in-process adapter that feeds a single state machine. The ACT queue becomes ingress-only; feedback flows through trade.events.
Shrink → act-normalizer
act-bridge-signal-processor keeps its normalising role but drops direct PG writes, persistence publishing, web.order.update emission, and the ACT-loopback.
Shrink → act-adapter
act-bridge becomes a pure outbound HTTP adapter. No PG writes, no persistence publish. Token fetch gets a distributed lock.
Clean → signal-out
Delete the legacy controller pair; one consumer per queue. Eliminates the non-deterministic dual-consumer hazard on web-orders and ftl-signal.
Extract → projectors
ui-projector and notification-projector subscribe to trade.events and own web.order.update and notify as single writers.
Keep → signal-junction
Entry normaliser for MT / Social stays roughly as-is. defect:* in Redis is kept (repair path joins across other Redis records); a lightweight signals.defect.recorded event is added alongside each write, for backlog alerting only.
Keep → persistence-service
Remains the only writer for signal_processor.signals and signal_processor.trade_logs. No more direct PG writes from other services.
P1 One writer per topic
Every RMQ topic has exactly one producer service. Shared producers (today: signal_orders, web.order.update, ACT) are the root cause of the cyclic loop and dedup hazards.
P2 Commands ≠ events
Addressed commands (PlaceOrderCommand) go to a specific service; events (OrderPlaced) are past-tense, broadcast facts. The ACT queue today mixes both — that is why it loops.
P3 trade_id everywhere
Correlation id generated at ingress, propagated via RMQ headers, logged at every hop, indexed in Redis and PG. Reconstructing a trade becomes a single query instead of an 8-log grep.
P4 Hot path is Postgres-free
Only persistence-service writes PG. act-bridge and act-bridge-signal-processor stop writing directly. Redis is the live state; PG is the async shadow.
IDRisk todayResolved by
R1Cyclic ACT loop — 3 publishers, 1 consumer, re-entry unclearACT queue becomes ingress-only (single writer: act-normalizer); feedback flows via trade.events. No topic is both ingress and feedback.
R2Dual consumers on web-orders + ftl-signal cause non-deterministic processingDelete legacy path. One controller per queue. Feature-flag cutover.
R3Hot-path PG writes from act-bridge + act-bridge-signal-processorAll PG writes go through persistence-service only.
R4RPC over RMQ on web-orders (30 s expiration) with dual-write riskDirect REST endpoint on trading-engine for web orders; event emission for downstream. No self-loop.
R5No correlation id — debugging a trade requires grep across 8 log streamsCanonical envelope with trade_id / event_id; enforced at ingress; logged everywhere.
R6web.order.update and notify emitted by multiple services with no ownershipProjections own these as single writers (ui-projector, notification-projector).
R7Redis / PG consistency unverified; "rebuild from PG" asserted but untestedpersistence-service is the single writer; Redis treated as disposable; rebuild drill scheduled.
R8Two near-identical WS relays (order-update, act-signal-processor)Merge into broker-ingress with per-driver config.
R9Token fetch race inside act-bridgeDistributed lock (Redis SETNX) around token refresh; single-flight per (platform, account).
R10Defect / repair loop has no alert — stuck signal-synchronizer silently drops when 24 h TTL expiresKeep Redis defect:* (repair needs cross-record joins). Add a signals.defect.recorded event alongside each write; alerting subscribes to it and pages on-call when backlog depth or age crosses a threshold.

Click an endpoint to expand its request & response. Scope is the trading flow only — used endpoints from gap_analysis/api_reference_used_only_2026-04-22.md. Health endpoints (/health*, /redis-health) are omitted — present on every service and consumed by infra only.

zulu3.0-act-signal-processor · zulu3.0-order-update · identical ActUpdateController in both · WebSocket control plane

GET /act-update/health both · plugins/act-update/act-update.controller.ts:50
Response
{ "success": true, "message": "ACT Update plugin is running", "timestamp": "2026-04-22T…" }

zulu3.0-act-bridge-signal-processor · RMQ consumer (act.order) + ops health

GET /act-order/health act-bridge-signal-processor · plugins/act_order/act_order.controller.ts:1864
Response
{
  "success": true,
  "data": { "status": "healthy", "service": "act-order", "timestamp": "…", "uptime": 1234 },
  "timestamp": "…"
}

zulu3.0-act-bridge · order operations · each endpoint validates & publishes to an order.* RMQ routing key

POST /pending/order act-bridge.controller.ts:143
Side effect: calls actBridgeService.placePendingOrder()
Request · body (PendingOrderPayload)
{
  "userEmail": "string",
  "symbol": "string",
  "quantity": 0,
  "side": "BUY | SELL",
  "account_id": "string",
  "price": 0,
  "stop": 0,
  "limit": 0,
  "trail": 0,
  "broker_platform": "ACT | HANKO | FXVIEW",
  "tag": "string?",
  "tempOrderId": "string?",
  "order_id": "string?",
  "user_id": "string?",
  "algogems_account_address": "string?"
}
Response
{ "success": true, "data": { /* service result */ } }
POST /place/order act-bridge.controller.ts:162
Publishes RMQ: order.place.market
Request · body (PlaceOrderPayload)
{
  "userEmail": "string",
  "symbol": "string",
  "quantity": 0,
  "side": "BUY | SELL",
  "account_id": "string",
  "stop": 0,
  "limit": 0,
  "trail": 0,
  "broker_platform": "ACT | HANKO | FXVIEW",
  "tag": "string?"
}
Response
{ "success": true, "data": { "success": true, "message": "Market order queued for processing", "orderType": "market_order" } }
POST /modify/order act-bridge.controller.ts:190
Publishes RMQ: order.modify
Request · body
{ "userEmail": "string", "order_id": "string", "price": 0, "quantity": 0, "tag": "string?" }
Response
{ "success": true, "data": { "orderType": "modify_order" } }
POST /cancel/order act-bridge.controller.ts:218
Publishes RMQ: order.cancel
Request · body
{ "userEmail": "string", "order_id": "string" }
Response
{ "success": true, "data": { "orderType": "cancel_order" } }
POST /place/stop act-bridge.controller.ts:246
Publishes RMQ: order.place.stop
Request · body
{
  "userEmail": "string", "symbol": "string", "quantity": 0, "side": "BUY | SELL",
  "account_id": "string", "stop": 0,
  "broker_platform": "ACT | HANKO | FXVIEW", "tag": "string?",
  "commentary": "string?", "trade": "string?", "order": "string?"
}
Response
{ "success": true, "data": { "orderType": "stop_order" } }
POST /place/trial act-bridge.controller.ts:274
Publishes RMQ: order.place.trail
Request · body
{
  "userEmail": "string", "symbol": "string", "quantity": 0, "side": "BUY | SELL",
  "account_id": "string", "trail": 0,
  "broker_platform": "ACT | HANKO | FXVIEW", "tag": "string?",
  "commentary": "string?", "trade": "string?", "order": "string?"
}
Response
{ "success": true, "data": { "orderType": "trail_order" } }
POST /place/limit act-bridge.controller.ts:302
Publishes RMQ: order.place.limit
Request · body
{
  "userEmail": "string", "symbol": "string", "quantity": 0, "side": "BUY | SELL",
  "account_id": "string", "price": 0,
  "broker_platform": "ACT | HANKO | FXVIEW", "tag": "string?",
  "commentary": "string?", "trade": "string?", "order": "string?"
}
Response
{ "success": true, "data": { "orderType": "limit_order" } }
POST /close/trade act-bridge.controller.ts:332
Side effect: calls actBridgeService.closeTrade() synchronously
Request · body
{
  "algogems_account_address": "string",
  "userEmail": "string",
  "trade": "string",
  "quantity": 0,
  "closeType": "H | N",
  "tag": "string?",
  "broker_platform": "ACT | HANKO | FXVIEW",
  "user_id": "string?"
}
Response
{ "success": true, "data": { /* service result */ } }
POST /open/trades act-bridge.controller.ts:435
Request · body
{ "userEmail": "string", "broker_platform": "ACT | HANKO | FXVIEW" }
Response
{ "success": true, "data": { /* open trades */ } }
↑ Back to overview
Module 06 · The Wires

Bridges

Two bridge layers connect the Zulutrade signal pipeline to external execution venues. ACT Bridge speaks the native ACT broker protocol. Social Bridge — Node middleware fans signals out to the terminal ecosystems (MT4, MT5, cTrader, Discord, Telegram) and is substantially more than a simple shim — it's a connector orchestrator with its own state and its own per-account process model for MetaTrader. See "How it works" below.

🔗 External integrations
ACT Brokers MT4 MT5 cTrader Discord Telegram
Bridges · outbound to venues Signal Pipeline from Trading Flow ACT Bridge act-bridge order-update speaks ACT native protocol receives fills (WebSocket) outbound + inbound on the ACT side Social Bridge — Node middleware Social Bridge — Node middleware Postgres · Redis · WebSocket · K8s pods connector orchestrator, not a shim ACT Brokers MT4 MT5 cTrader Discord Telegram fills (WS)

Both bridges side-by-side

This overview shows the two bridge subsystems fed from the Signal Pipeline. ACT Bridge (act-bridge outbound, order-update inbound over WebSocket) handles ACT-native brokers; Social Bridge (social-trader3.0-node-middleware) fans signals out to the terminal ecosystems. Switch to a detail tab for per-bridge flow.

ACT Bridge · outbound + inbound roundtrip Signal Pipeline signal-out 🐇 act.signal.out act-bridge ACT Brokers per-broker ACT env order-update act-bridge-signal-processor Signal Pipeline back into copy logic HTTP fills (WS) Outbound: act-bridge resolves the right broker ACT setup, reuses or fetches a session token, calls ACT trading APIs over HTTP. Inbound: ACT brokers push fills / partial fills / rejects back via WebSocket; order-update hands them to act-bridge-signal-processor. Return path rebuilds the event in the shape signal-processor expects, so the pipeline continues without a separate ACT-only integration.

ACT Bridge — outbound + inbound

Outbound: signal-out publishes to act.signal.out. act-bridge consumes, resolves the per-broker ACT environment, fetches/reuses a cached session token, and calls the broker's ACT trading APIs over HTTP. Order actions arrive under topics like market, limit, stop, trailing stop, pending orders, modify, cancel, close.

Inbound: ACT brokers push execution events (fills, partial fills, rejects) back over WebSocket. order-update keeps the socket alive, classifies each event, and publishes it onto the order topic. act-bridge-signal-processor picks it up, reconciles order/trade IDs, and republishes the event in the shape signal-processor already understands — so risk checks and copying logic continue through the normal pipeline.

Meta (MT4/MT5) · per-account pod orchestration Signal Pipeline 🐇 SIGNAL_OUT Social Bridge Node middleware · MT orchestrator PG · signal_out Redis · new_signal Kubernetes / Docker · one pod per MT account MT pod · acct 1 WS ↔ MT4 MT pod · acct 2 WS ↔ MT5 MT pod · acct 3 WS ↔ MT4 one per acct MT4 terminal MT5 terminal MT4 terminal mt-health-check dockerode / K8s client · spawn pod per account WebSocket frame {event: "v1_new-signal", data: [...]} is sent per pod · MT boots synchronously with 1s waits between accounts MT is the only connector with this pod-per-account model — other connectors are in-process (see Social / cTrader tabs)

Meta (MT4/MT5) — one pod per account

MT connectors have a runtime model that's unique within Social Bridge. For each trading account, Social Bridge uses dockerode (local Docker) or @kubernetes/client-node (cluster) to spawn a dedicated process: sanitising the name, allocating an incrementing port, auto-detecting the namespace from the mounted ServiceAccount, and starting an MT process inside that pod/container.

When a signal lands on SIGNAL_OUT, Social Bridge writes the row to its own Postgres, publishes on Redis new_signal, and the per-account handler pushes a WebSocket frame {event: "v1_new-signal", data: [...]} to the correct pod. The pod's MT process sends the order through to the terminal.

core/tasks/mt-health-check.js keeps tabs on each pod. MT boots synchronously with ~1s delays between accounts to avoid spawn races. This model is why MT is sometimes described as "really distributed" while other connectors are flat in-process singletons.

Social (Telegram / Discord) · in-process connectors Signal Pipeline 🐇 SIGNAL_OUT Social Bridge in-process singletons connectors/ (same process) telegram.js (telegraf + mtproto) discord.js v14 bot Telegram Discord 🐇 AI_MESSAGE OpenAI 2FA code extraction incoming msg w/ 2FA code extracted token

Social (Telegram / Discord) — in-process, plus 2FA loop

Outbound: signals from SIGNAL_OUT land in Social Bridge, which calls the correct in-process connector's .process(data) method. telegram.js runs a dual stack (telegraf for modern bot API, @mtproto/core for legacy MTProto) and joins after a 5-minute deferred start. discord.js v14 runs as a full Discord bot. Both are singletons, not per-account.

Inbound 2FA loop: when Telegram or Discord receives a login / verification message, the raw text is pushed onto the AI_MESSAGE RabbitMQ queue. A dedicated OpenAI worker (core/ai.js, running on a pool of API accounts from env) parses the message and extracts the 2FA token via regex. The extracted token is routed back to the connector that needs it — typically to complete an MT or cTrader login challenge — so sessions don't stall on 2FA.

cTrader · in-process singleton connector Signal Pipeline 🐇 SIGNAL_OUT Social Bridge Node middleware connectors/ctrade.js cTrader terminal native cTrader protocol One cTrader connector instance in the Social Bridge process · hydrated from DB on boot

cTrader — in-process singleton

Social Bridge runs one cTrader connector instance (connectors/ctrade.js, ~46KB implementation) in-process. When a signal arrives for a cTrader platform, the dispatcher calls connector.process(data) directly — no pod spawning, no extra hop. State is hydrated from Postgres on boot. The connector speaks cTrader's native protocol out to the terminal.

Session-level 2FA challenges (if any) go through the same OpenAI extraction loop described in the Social (TG/Discord) tab.

ACT Bridge

ServiceRoleStatusNotes
act-bridgeAdapter to ACT brokersupSpeaks ACT native protocol; outbound over HTTP using cached broker session tokens.
order-updateWebSocket consumer for broker order eventsupFills, partial fills, rejects; feeds back into the trading-flow pipeline via act-bridge-signal-processor.

Social Bridge

ServiceRoleStatusNotes
Social Bridge — Node middlewareTerminal-ecosystem bridgeupSeparate project (social-trader3.0-node-middleware). Fans signals out to MT4, MT5, cTrader, Discord, Telegram. Owns its own Postgres + Redis. Runs MT as per-account K8s/Docker pods. See "How it works" below.
🧭 Placement
The Bridges sit between the Trading Flow pipeline and the world outside. Signals flow outward via act-bridge and Social Bridge; execution events flow back in via order-update. ACT Bridge runs alongside the other Zulutrade services; Social Bridge is a separate project (social-trader3.0-*) also co-located on the Application Server.

Social Bridge — How it works

A closer look at social-trader3.0-node-middleware. This service isn't a thin protocol shim — it's a connector orchestrator, a signal dispatcher, and a per-account Kubernetes manager rolled into one.

⚠ State lives here
Social Bridge has its own PostgreSQL Database (sessions, signal log, MT server catalogue, account state) and its own Redis (pub/sub + per-account maps). This bridge uses a on-host Redis, while PostgreSQL server can be hosted with the Zulu 3.0 setup covered in Data & Infrastructure. An ops engineer touching either datastore needs to know which side they're on.
🤖 OpenAI 2FA extraction
Social Bridge subscribes to a dedicated AI_MESSAGE RabbitMQ queue and uses OpenAI (via a pool of API accounts configured in env vars) to extract 2FA verification codes from inbound broker / terminal messages. When a terminal emits a one-time code, it's forwarded through this pipeline and the extracted token is routed back to the connector that needs it — so login flows can complete without manual intervention. Niche but important feature; without it, MT / cTrader sessions stall on 2FA challenges.

Runtime model per connector

MT4 / MT5
One Docker container or Kubernetes Pod per trading account. Social Bridge uses dockerode (local) and @kubernetes/client-node (cluster) to sanitise names, allocate incrementing ports, auto-detect the namespace from the mounted ServiceAccount, spawn each MT process, and health-check it via core/tasks/mt-health-check.js. Communication is WebSocket-over-TCP. This is why MT is "really distributed" and the other connectors are not.
cTrader · Telegram · Discord
In-process connectors. cTrader runs as a singleton instance. Telegram uses a dual stack (@mtproto/core legacy + telegraf) and joins after a 5-minute deferred start. Discord uses discord.js v14 with a full bot. They're implemented under connectors/ and called directly from the in-process dispatch path — no pod spawning.

Data path

Signal → Social Bridge → terminal signal-out 🐇 Rabbit · SIGNAL_OUT Social Bridge Node middleware PG · signal_out row Redis · new_signal MT pod (WS frame) in-proc .process() signal lands in Postgres → published on Redis new_signal → platform-specific handler picks it up MT handlers push a WebSocket frame to the per-account pod; others call connector.process() directly

RabbitMQ queues used by Social Bridge

QueueDirectionPurpose
SIGNAL_OUTconsumeMain intake — outbound signals from signal-out that need to land on a terminal.
SIGNAL_INpublishInbound signals originating from terminal events (e.g., a Telegram message or a Discord command) pushed back into the trading pipeline.
SIGNAL_VERIFICATIONconsume / publish2FA / verification codes moving between the connector that needs them and whichever service can fulfil them.
AI_MESSAGEconsumeMessages to be parsed by the OpenAI integration (see 2FA extraction callout above).
SESSIONSconsume / publishSession lifecycle events — connector boot, auth, disconnect.
SIGNAL_INFOconsume / publishSignal metadata / info messages paralleling the main signal flow.
ERRORpublishStructured errors (e.g., a failed session boot with full session context) routed out for observability.

All point-to-point queues on the Social Trader (legacy) broker — no topic exchanges from this service. Read and write channels are separate.

HTTP API surface

/api/copy-trading/v1/*
Called by Copy Trading Core. login, logout, send-signal. Auth policy: auth-copy-trading-core.
/api/admin/v1/*
Admin portal — 9 handlers covering server state, session & signal queries, and connector reboot. Auth policy: auth-admin-portal.
ws://… (connectors over WebSocket)
Connector lifecycle — 14+ handlers: check-auth, logout, push-info (bulk/single), push-order, telegram / discord / error logging, verification, history. Not HTTP — resolved in core/websocket.js against app.connectors_routes. Auth: SHA-512(session.id + session.created_at).

Architecture improvements — Bridges consolidation

Target-state direction for the bridge services that back Module 05 Trading Flow. Today three services cooperate on the outbound path: zulu3.0-act-bridge (order execution to ACT brokers), zulu3.0-act-bridge-signal-processor (address/symbol normalization, order dispatch staging), and social-trader3.0-node-middleware (multi-venue ingestion for MT4/5, cTrader, Telegram, Discord). They share duplicated NestJS scaffolding, identical hardcoded DB password fallbacks, and a rabbit topology where the two ACT services sit on different exchanges and don't talk to each other directly. Each tab groups the proposal differently: the decisions, the principles, the risks each decision resolves, and the current APIs.

Keep act-bridge separate (broker specialist)
Don't merge act-bridge into the signal-processor or node-middleware. It speaks the ACT-native HTTP/XML-RPC protocol; future brokers (Alpaca, IB) warrant their own <broker>-bridge. Its 38 endpoints (root hot-path, /act-trader/*, /v3/admin/api/*) stay where they are.
Unify the two rabbit exchanges
Today act-bridge.controller.ts:27 uses exchange act-bridge-orders; signal-processor act_order.controller.ts:13 uses signalData. They can't share a flow. Pick one exchange (proposal: zulu.act.orders) and have both services bind to it.
Standardize the topic taxonomy
act-bridge binds 8 topics (order.place.{market,limit,stop,trail,pending}, order.{modify,cancel,close}); signal-processor binds only order (a catch-all). Replace the catch-all with the same topic set so downstream subscribers filter by intent, not by JSON parsing.
Fold signal-processor into act-bridge
The processor is ~4 HTTP endpoints (health/status/process/stats) + address-patcher + symbol-patcher + a single rabbit consumer. Everything it does belongs in act-bridge as a pre-execution middleware stage. Less network, one deploy unit, fewer hardcoded DB fallbacks.
Extract the app.module.ts scaffold
Both NestJS services have byte-identical DB bootstrap — including the literal password fallback 'kfjNr@@CNINmPiwgp' on line 26. Move this to a shared @stackflow-bootstrap package and remove the fallback entirely.
Keep node-middleware as the ingestion specialist
It owns MT4/5 container orchestration, cTrader OAuth, Telegram/Discord bots, and WebSocket fan-in. Different concern, different shape (Express + custom action dispatcher, not NestJS). Don't merge; instead define a clean contract between it and the ACT/broker bridges.
Finish graceful shutdown in node-middleware
index.js:33,34,49,50 carry TODO TURN OF DOCKERS MT4 and MT5 and TODO DISCOINNECT ALL LISTNERS in both SIGINT and SIGTERM handlers. During deploys this means orphaned MT containers and zombie socket listeners.
Replace the WebSocket hash token
core/websocket.js:62 uses SHA-512(session.id + session.created_at) as the connector auth token — no expiry, no rotation, same token forever. Move to short-lived JWT (RS256, 15-min TTL) issued by the session creator.
Retire the commented-out BullModule
Both NestJS services' app.module.ts contain a commented-out BullModule.forRootAsync. It's a lie about the architecture — either wire it up for Redis-backed retries, or delete the dead import to stop misleading future readers.
One bridge per venue
Each broker protocol gets its own service (act-bridge, future alpaca-bridge, etc.). Don't polymorphize across brokers — the protocols diverge too much. Ingestion is the only layer that abstracts over venues.
Sync for read-paths, async for writes
act-bridge already follows this: POST /pending/order, /close/trade, /open/trades are synchronous; order placement is rabbit-driven. Keep the boundary clear — don't let a "quick win" sync call sneak onto a write path.
Topic is the contract
Downstream consumers bind to order.place.market, not to "whatever payload type looks like a market order." Producers must publish on the right topic even when it costs code. No catch-all topics.
Secrets out of code, always
No fallback values for passwords, tokens, or API keys anywhere in app.module.ts / configs. Fail loud on startup if an env var is missing — silent fallbacks hide config drift across environments.
Graceful shutdown is a hard requirement
Every bridge ships with a working SIGTERM handler: drain rabbit consumers, close WebSocket sessions, stop child docker containers, flush logs. Deploys should be boring.
Correlation-id end-to-end
ID flows from node-middleware ingestion → rabbit message header → act-bridge HTTP call to ACT → log lines. Today it breaks at every hop. Axios/HTTP interceptors + rabbit publish wrappers carry it.
Risk todayHow the proposal resolves it
Identical DB password fallback 'kfjNr@@CNINmPiwgp' in two repos (app.module.ts:26)Single @stackflow-bootstrap helper; no fallback — fail loud on startup
act-bridge + signal-processor sit on different rabbit exchanges (act-bridge-orders vs signalData) — they can't share flowMerge on one exchange (zulu.act.orders); every producer/consumer binds to the same topics
signal-processor uses catch-all topic order — subscribers must parse payload to routeUse the same 8-topic taxonomy as act-bridge; bind by intent, not by shape
Two NestJS services for what is conceptually one pipeline stage (pre-exec patching + execution)Fold signal-processor into act-bridge as a pre-execution middleware; one deploy unit
WebSocket connector token = static SHA-512 of session fields; no expiryShort-lived JWT (RS256, 15-min TTL) issued by the session creator
SIGINT/SIGTERM in node-middleware/index.js carry 4 unfinished TODOs — orphaned MT4/MT5 docker pods during deploysImplement docker-cleanup + listener-disconnect in shutdown hooks; add an integration test that asserts clean termination
Commented-out BullModule in both NestJS services misleads readers about the retry storyEither wire it up or delete it; no "ghost architecture" in imports
Existing HTML claimed /api/connectors/v1/* was HTTP — actually WebSocketSurface corrected in this module; connectors labeled ws://… with auth mechanism called out
No correlation-id across ingestion → rabbit → execution hopsHTTP/rabbit wrappers carry x-request-id; logs grep across services

Click an endpoint to expand. All lists are sourced directly from the controllers' @Controller / @Post / @Get decorators — not inferred.

zulu3.0-act-bridge · hot-path orders @Controller() (root)

POST /pending/order act-bridge.controller.ts:143 · sync → ACT API
DTO: PendingOrderPayloadMode: synchronous HTTP to ACT
Request · body
{ /* PendingOrderPayload — see act-bridge.dto.ts */ }
Response · 200
{ /* ACT response passthrough */ }
POST /place/order act-bridge.controller.ts:162 · async → rabbit order.place.market
DTO: PlaceOrderPayloadPublishes topic: order.place.market
Request · body
{ /* PlaceOrderPayload */ }
Response · 202
{ "status": "queued", "correlationId": "…" }
POST /place/limit act-bridge.controller.ts:302 · async → order.place.limit
DTO: PlaceLimitPayload
Request · body
{ /* PlaceLimitPayload */ }
POST /place/stop act-bridge.controller.ts:246 · async → order.place.stop
DTO: PlaceStopPayload
Request · body
{ /* PlaceStopPayload */ }
POST /place/trial act-bridge.controller.ts:274 · async → order.place.trail
DTO: PlaceTrailPayload⚠ Typo: route is /place/trial, topic is order.place.trail
Request · body
{ /* PlaceTrailPayload */ }
POST /modify/order act-bridge.controller.ts:190 · async → order.modify
DTO: ModifyOrderPayload
Request · body
{ /* ModifyOrderPayload */ }
POST /cancel/order act-bridge.controller.ts:218 · async → order.cancel
DTO: CancelOrderPayload
Request · body
{ /* CancelOrderPayload */ }
POST /close/trade act-bridge.controller.ts:332 · sync → ACT API
DTO: CloseTradePayload
Request · body
{ /* CloseTradePayload */ }
POST /trade/place/limit act-bridge.controller.ts:351
Trade-bound limit order
POST /trade/place/stop act-bridge.controller.ts:379
Trade-bound stop order
POST /trade/place/trail act-bridge.controller.ts:407
Trade-bound trailing stop
POST /open/trades act-bridge.controller.ts:435 · sync → ACT API
DTO: OpenTradesPayload
Request · body
{ /* OpenTradesPayload */ }
Response · 200
{ /* array of open trades */ }

zulu3.0-act-bridge · trader/customer mgmt @Controller('act-trader')

GET /act-trader/health act-trader.controller.ts:41
Response · 200
{ "data": { /* healthStatus */ } }
POST /act-trader/customer/create act-trader.controller.ts:63
Create customer in ACT
POST /act-trader/customer/trader/create act-trader.controller.ts:86
Create trader under customer
POST /act-trader/customer/trader/modify act-trader.controller.ts:110
Modify trader attributes
POST /act-trader/self/trader/create act-trader.controller.ts:133
Self-serve trader creation
POST /act-trader/account/create act-trader.controller.ts:157
Open trading account
POST /act-trader/account/modify act-trader.controller.ts:180
Modify account
GET /act-trader/account/get act-trader.controller.ts:203
Fetch account
POST /act-trader/act/change/password act-trader.controller.ts:227
Change ACT password
POST /act-trader/change/act/leverage act-trader.controller.ts:250
Adjust leverage
POST /act-trader/deposit/balance act-trader.controller.ts:274
Credit balance
POST /act-trader/withdraw/amount act-trader.controller.ts:297
Withdraw amount
POST /act-trader/adjustment/balance act-trader.controller.ts:320
Balance adjustment
POST /act-trader/change/balance act-trader.controller.ts:343
Change balance
POST /act-trader/change/bonus act-trader.controller.ts:367
Change bonus
POST /act-trader/get/margin act-trader.controller.ts:391
Margin query
POST /act-trader/deposit/history act-trader.controller.ts:415
Deposit history
POST /act-trader/account/history act-trader.controller.ts:438
Account history
POST /act-trader/get/doc act-trader.controller.ts:461
Document retrieval
POST /act-trader/get/open/trades act-trader.controller.ts:485
Open trades
POST /act-trader/get/open/orders act-trader.controller.ts:507
Open orders
POST /act-trader/get/gethistory act-trader.controller.ts:529
Trade history

zulu3.0-act-bridge-signal-processor @Controller('act-order')

GET /act-order/health act_order.controller.ts:1864
Response · 200
{ "status": "ok" }

social-trader3.0-node-middleware · HTTP

HTTP /api/admin/v1/* core/server.js:163 · auth: auth-admin-portal
Handlers (actions/admin-portal/v1/)
POST  /api/admin/v1/login               post-login.js
POST  /api/admin/v1/logout              post-logout.js
POST  /api/admin/v1/drop-session        drop-session.js
GET   /api/admin/v1/servers             get-servers.js
GET   /api/admin/v1/sessions            get-sessions.js
GET   /api/admin/v1/signals-in          get-signals-in.js
GET   /api/admin/v1/signals-out         get-signals-out.js
GET   /api/admin/v1/users               get-users.js
POST  /api/admin/v1/reboot-connector    reboot-connector.js
HTTP /api/copy-trading/v1/* core/server.js:201 · auth: auth-copy-trading-core
Handlers (actions/copy-trading-core/v1/)
POST  /api/copy-trading/v1/login                       login.js
POST  /api/copy-trading/v1/logout                      logout.js
POST  /api/copy-trading/v1/profile                     profile.js
GET   /api/copy-trading/v1/account-profile             account-profile.js
POST  /api/copy-trading/v1/account-info                account-info.js
POST  /api/copy-trading/v1/default-volume              default-volume.js
POST  /api/copy-trading/v1/send-signal                 send-signal.js
GET   /api/copy-trading/v1/mt-servers                  metatrader-servers.js
GET   /api/copy-trading/v1/mt-account-symbols          metatrader-account-symbols.js
GET   /api/copy-trading/v1/ctrade-account-symbols      ctrade-account-symbols.js
GET   /api/copy-trading/v1/binance-symbols             binance-symbols.js
GET   /api/copy-trading/v1/telegram-chats-list         telegram-chats-list.js
POST  /api/copy-trading/v1/telegram-set-chats          telegram-set-chats.js
POST  /api/copy-trading/v1/telegram-verification-code  telegram-verification-code.js

social-trader3.0-node-middleware · WebSocket (connectors)

WS ws://…/v1/{action} core/websocket.js · auth: SHA-512(session.id + created_at)
Resolved in: websocket.js:162-166 against app.connectors_routes⚠ Gap: token has no expiry / rotation
Actions (actions/connectors/v1/)
push-order            inbound signal (Metatrader / Binance / Telegram / Discord / Ctrade)
push-info             single account-info update
push-info-bulk        batched account updates
telegram-signal       inbound Telegram signal
telegram-signal-history  history sync
discord-signal        inbound Discord signal
read-signal           signal lookup
history-signals       session signal history
check-auth            session validation
logout                terminate session
verification          signal verification
set-symbols           symbol set update
error-logs            connector error stream
profile               profile sync

social-trader3.0-node-middleware · RabbitMQ consumer

AMQP consume RABBITMQ_SIGNAL_OUT_QUEUE core/rabbitmq.js:69
Dispatches to: app.rabbit_routes['v1'][queueName]
Consumer (actions/rabbit/v1/)
save-out-signals   persists verified signals emitted by the pipeline

RabbitMQ topology (outbound side)

AMQP exchange: act-bridge-orders act-bridge.controller.ts:27
Queue: act.signal.out (durable)Prefetch: 50
Bound topics
order.place.market     → handleOrderMessage → ACT (market)
order.place.limit      → handleOrderMessage → ACT (limit)
order.place.stop       → handleOrderMessage → ACT (stop)
order.place.trail      → handleOrderMessage → ACT (trail)
order.place.pending    → placePendingOrder
order.modify           → processModifyOrder
order.cancel           → processCancelOrder
order.close            → closeTrade
AMQP exchange: signalData signal-processor act_order.controller.ts:13 · ⚠ separate exchange from act-bridge
Queue in: act.orderTopic: order (catch-all)Publishes to queue: ACT
Consolidation note
Proposal: retire the 'signalData' exchange; have signal-processor
bind to the same 'act-bridge-orders' exchange with the same 8 topics.
One exchange, one topic taxonomy.

Impact on existing flows

  • Module 05 Trading Flow — no user-visible change. The hot-path (act-bridge-orders exchange) keeps its topology; only the pre-exec stage (signal-processor) is folded in.
  • Module 02 Account Connection — node-middleware's WebSocket auth change requires the client-side connector to request a JWT at session creation instead of synthesizing the SHA-512 token locally.
  • Deploy pipeline — graceful-shutdown completion in node-middleware means cleaner rolling deploys; no more orphaned MT4/MT5 pods.
📦 Migration shape
Roughly 3–5 days. Days 1–2: extract the shared app.module.ts scaffold and remove the hardcoded DB password fallback from both NestJS services. Days 3–4: fold signal-processor's topics + patcher services into act-bridge, retire the signalData exchange. Day 5: replace the WebSocket SHA-512 token with a short-lived JWT and finish the SIGINT/SIGTERM handlers in node-middleware/index.js.
↑ Back to overview
Module 07 · Analytics

Analytics

How trading history becomes charts. Trades flow out of ACT-281 via the auth-service exports plugin, land on disk as files, get ingested and transformed by social-analytics into ClickHouse, and finally surface through stats-service to the client and admin UIs.

🔗 External integrations
ACT-281 ClickHouse
Analytics · trade history → charts ACT-281 trade source auth-service exports plugin (also in Onboarding) files exported trades social-analytics ClickHouse stats-service client / admin UI trades exports reads writes queries charts

Services

auth-service · also in Onboarding
Listed here for the exports plugin. The plugin exports trades from ACT-281 and writes them to disk as files; social-analytics picks them up from there. The rest of auth-service's responsibilities (SSO, broker-auth, OTP, sessions) are covered in User Onboarding.
social-analytics
Ingests the exported trade files, transforms them, and writes the result into ClickHouse. Stable interface for everything downstream — stats-service queries here, not ClickHouse directly.
stats-service
Analytics API layer — chart/metrics endpoints (balance, instruments, holding period, top instruments, trade efficiency, overall stats, profit calendar, portfolio/ROI, leader comparison, trade history). Reads analytics through social-analytics; enriches results with calls to connector-hub, users-service, temp-trader.
🚧 Next phase — extract exports plugin
The exports plugin is currently bundled inside auth-service. Planned work: lift it into a dedicated service so auth is just auth, and the exports path has its own release cadence and ownership. No action for this doc; noting it so future readers know the module's internal boundary will move.

Architecture improvements

Four tabs covering the proposed direction for the analytics services: Key decisions names what each of auth-service (exports), stats-service, and social-analytics becomes; Design principles states the rules the new layout is built on (thin aggregator, single analytics read path, auth at the gateway, one export queue); Risks resolved ties each change to a concrete current-system risk with evidence from code; APIs is an index of the used HTTP endpoints in scope. Full rationale and citations in gap_analysis/analytics_gap_analysis_2026-04-22.md.

Extract → analytics-exporter-service
Lift the exports plugin (v1 + v2), sync-trade-history, and the BullMQ scheduler out of auth-service. New service owns the 4 upstream HTTP deps (trader / rewards / connector-hub / trading-fundamentals), the DownloadRequests Postgres table, the EXPORT_FILE_PATH storage contract, and the info.manual RMQ consumer. auth-service keeps SSO / sessions / verification only.
Consolidate → v1 + v2 export surfaces
Today: two queues (exports-queue, exports-v2-queue), two workers, two controllers, same logical pipeline. Pick one; feature-flag cutover; delete the other.
Fix → auth on stats-service
Zero @UseGuards across all 26 endpoints. Filter CRUD (/filters, PUT/DELETE /filters/:id) accepts user_id as a query param — trivially spoofable. Gateway-level auth guard; derive user_id from JWT.
Keep → stats-service (thin)
HTTP aggregation layer is the right shape. Don't grow it into a cache or business-logic layer. If caching is needed for the social-analytics calls, add a Redis cache tier but keep the contract.
Keep → social-analytics (boundary)
External service. It is the only analytics read path — no direct ClickHouse reads from stats-service or from the new exporter. The boundary is the reason stats-service has survived as a thin layer.
Clean → dormant RMQ / Redis in stats-service
rabbit-mq.module.ts and redis.module.ts are wired and health-indicated, but no consumer / publisher / key access exists in plugin code. Either remove the modules or wire the cache for real.
Document → DownloadRequests + 4 upstream deps
The diagram shows a single arrow from ACT-281. In code, the exports plugin fans out to trader, rewards, connector-hub, and trading-fundamentals. State lives in a Postgres DownloadRequests table. All four deps + the table belong in the diagram.
Decide → ingestion trigger authority
Today the pipeline is partly file-watch (social-analytics pulls CSVs) and partly HTTP-trigger (POST /stats/import/trigger calls {ANALYTICS_BASE_URL}/api/import/trigger). Choose one as authoritative and document.
P1 Stats-service stays thin
Pure HTTP aggregation and light enrichment. All heavy analytics (growth, drawdown, ROI, calendars) are computed inside social-analytics. No business-logic drift into the gateway layer.
P2 One analytics read path
social-analytics is the only way to reach ClickHouse. Never add a direct ClickHouse client to stats-service or the new analytics-exporter-service. The stable interface is what lets ClickHouse evolve independently.
P3 One scheduler, one export queue
Collapse v1 / v2 parallel queues and workers. Queue names describe the job (exports, exports-scheduler), not the version. Deprecate the legacy path behind a feature flag, then delete.
P4 Auth at the gateway
No endpoint on stats-service accepts user_id without JWT validation. user_id is derived from the token, never from the query string. Filter CRUD is the most exposed case today and is the first target.
IDRisk todayResolved by
R1Analytics pipeline bundled with auth-service — a crashing export worker takes down loginExtract exports plugin + scheduler + sync-trade-history to analytics-exporter-service. auth-service keeps only SSO / sessions / verification.
R2stats-service filter CRUD accepts user_id as a query param with no auth — spoofableGateway-level auth guard; user_id derived from JWT. Zero @UseGuards today across all 26 endpoints.
R3v1 + v2 export surfaces coexist — two queues, two workers, two controllers, same logical pipelinePick one, cutover behind a feature flag, delete the other. Halves the ops surface.
R4Exports plugin has 4 upstream HTTP deps (trader, rewards, connector-hub, trading-fundamentals) — none in the diagramDiagram update; extracting the service makes the deps an explicit contract of analytics-exporter-service.
R5DownloadRequests Postgres table — the export-request system of record — is invisible in the diagramAdd to the diagram as the state store for the export feature. It holds status, file_token, error_message, timestamps.
R6RabbitMQ + Redis infrastructure in stats-service is wired and health-indicated but unusedRemove the modules, or wire them for real (e.g. Redis cache tier for social-analytics calls). Dead infrastructure drifts out of date.
R7sync-trade-history RMQ consumer (queue info.manual) is unmentioned in the Analytics moduleSurface in the diagram or move to the new service alongside the exports plugin.
R8Ingestion trigger is ambiguous — partly file-watch, partly HTTP (POST /stats/import/trigger)Pick one authoritative trigger. Document. If HTTP, retire the file-watcher; if file-watch, retire the trigger endpoint.

Click an endpoint to expand its request & response. Scope is the analytics module only. Health endpoints (/health*, /redis-health) omitted. Full citations in gap_analysis/analytics_gap_analysis_2026-04-22.md.

zulu3.0-auth-service · exports plugin (app/express/plugins/exports/)

POST /export auth-service · exports/export.controller.ts:31
Handler: ExportController.createexportService.create() Side effect: enqueues export-job on BullMQ queue exports-queue
Request · body (v1 DTO)
{
  "type": "history | open_trades | transactions | daily_balance",
  "account_id": "string",
  "account": "string",
  "user_id": "string",
  "from": "ISO | YYYY-MM-DD",
  "till": "ISO | YYYY-MM-DD"
}
Response · 202
{ "data": { /* download_request row */ } }
GET /export auth-service · exports/export.controller.ts:167
Request · query
{ "user_id", "account_id", "account", "status", "type", "from", "till", "sortField", "sortOrder", "page", "pageSize" }
Response · 202
{ "data": [ /* download_request rows */ ] }
GET /export/get-file auth-service · exports/export.controller.ts:186
Side effect: streams the CSV (with UTF-8 BOM) resolved by fileToken
Request · query
{ "fileToken": "string (required)", "user_id": "string" }
Response · 200
CSV stream (text/csv)
POST /export/recover auth-service · exports/export.controller.ts:210
Side effect: exportSchedulerService.recoverDataForDateRange() — re-runs the scheduler jobs for a date range
Request · body
{
  "start_date": "ISO",
  "end_date": "ISO",
  "types": ["history" | "open_trades" | "transactions" | "daily_balance"],
  "isResyncRequest": true
}
Response · 200
{ "message": "…", "data": { "daysSucceeded": 0, "daysProcessed": 0 } }
POST /export/v2 auth-service · exports/export-v2.controller.ts:18
Side effect: enqueues export-v2-job on BullMQ queue exports-v2-queue
Request · body
{
  "type": "history | open_trades | transactions | reward_transactions | reward_withdrawals",
  "algogems_account_address": "string",
  "user_id": "string",
  "account": "string",
  "from": "YYYY-MM-DDTHH:mm:ss",
  "till": "YYYY-MM-DDTHH:mm:ss"
}
Response · 202
{ "data": { /* download_request row */ } }
GET /export/v2 auth-service · exports/export-v2.controller.ts:121
Request · query
{ "id", "account_id", "account", "algogems_account_address", "user_id", "status", "type", "from", "till", "sortField", "sortOrder", "page", "pageSize" }
Response · 200
{ "data": [ /* download_request rows */ ] }
GET /export/v2/get-file auth-service · exports/export-v2.controller.ts:135
Request · query
{ "fileToken": "string (required)", "user_id": "string" }
Response · 200
CSV stream
POST /sync-trade-history auth-service · sync-trade-history/sync-trade-history.controller.ts:54
RMQ consumer (sibling): queue info.manual Calls: trader service (TRADER_HOST) for sync
Request · body
(see SyncTradeHistoryDto — user_id / account / from / till)
Response
{ /* sync status */ }

zulu3.0-stats-service · chart / portfolio aggregator · no auth in code ⚠

POST /stats/import/trigger stats-service · stats.controller.ts:81
Proxies to: {ANALYTICS_BASE_URL}/api/import/trigger (social-analytics)
Request · body (SocialAnalyticsImportTriggerDto)
{ "force": false, "resyncOnly": false }
Response
(passthrough from social-analytics)
GET /charts/balance stats.controller.ts:118
Proxies to: social-analytics /api/charts/balance
Request · query (BalanceChartQueryDto)
{ "user_id", "account_id", "account", "from", "till", "groupBy": "day | week | month", "…filters" }
Response
{ /* balance series: [{ ts, balance, deposit, withdrawal, equity }] */ }
GET /charts/balance/all stats.controller.ts:147
Side effect: 4 parallel calls to social-analytics (balance + deposit + withdrawal + equity)
Response
{ "balance": [...], "deposit": [...], "withdrawal": [...], "equity": [...] }
GET /charts/instruments stats.controller.ts:176
Response
{ /* per-instrument breakdown: trades, PnL, win rate */ }
GET /charts/accounts stats.controller.ts:205
Response
{ /* account-level trading summary */ }
GET /charts/holding-period stats.controller.ts:248
Response
{ /* avg holding time per trade */ }
GET /charts/top-instruments stats.controller.ts:277
Response
{ "gainers": [...], "losers": [...] }
GET /charts/trade-efficiency stats.controller.ts:306
Response
{ /* win/loss ratio, avg win/loss, profit factor */ }
GET /charts/overall-stats stats.controller.ts:335
Response
{ /* overall PnL, win rate, number of trades */ }
GET /charts/my-statistics stats.controller.ts:364
Enriches with: users-service /users/get/user_id/{userId} for last_login_at
Response
{ /* growth, drawdown, equity, last_login_at */ }
GET /charts/my-statistics/all stats.controller.ts:393
Side effect: 6 parallel calls to social-analytics (growth, drawdown, overall, holding, roi-drawdown, max-open-trades)
Response
{ "growth": ..., "drawdown": ..., "summary": ... }
GET /charts/roi-daily stats.controller.ts:422
Response
{ /* daily ROI series */ }
GET /charts/roi-simulation stats.controller.ts:451
Internal: wraps /charts/roi-daily
Request · query
{ "amount": 0, "timeframe": "1M | 3M | 6M | 1Y | ALL" }
Response
{ /* simulated equity curve */ }
GET /charts/profit-calendar/months stats.controller.ts:480
Request · query
{ "year": 2026 }
Response
{ /* monthly profit calendar */ }
GET /charts/profit-calendar/days stats.controller.ts:509
Request · query
{ "year": 2026, "month": 4 }
Response
{ /* daily profit calendar */ }
GET /charts/profit-calendar/day stats.controller.ts:538
Request · query
{ "date": "YYYY-MM-DD" }
Response
{ /* single day profit details */ }
GET /charts/trade-pnl-history stats.controller.ts:567
Response
{ /* per-trade PnL with pips / dollars */ }
GET /charts/trade-history-combined stats.controller.ts:596
Enriches with: connector-hub /closed-orders + /closed-orders/instruments; Postgres (copyList)
Response
{ "closedOrders": [...], "tradePnl": [...], "instruments": [...], "copyList": [...] }
GET /portfolio/summary stats.controller.ts:627
Enriches with: temp-trader /account/get + social-analytics /api/portfolio/summary + users-service /users/get/user_id/{userId}
Response
{ "account": {...}, "performance": {...}, "followers": {...}, "last_logged_in": "ISO" }
GET /leader-comparison stats.controller.ts:659
Response
{ /* leader comparison stats */ }
GET /leader-overview stats.controller.ts:688
Response
{ /* ROI, drawdown, AUM, copiers */ }
GET /filters stats.controller.ts:717 · user_id from query (spoofable ⚠)
DB: analytics.users_saved_filters
Request · query (SavedFiltersUserQueryDto)
{ "user_id": "string" }
Response
UsersSavedFilterRow[]
GET /filters/by-name/:name stats.controller.ts:745
Request · path + query
path: name
query: { "user_id" }
Response
UsersSavedFilterRow | 404
POST /filters stats.controller.ts:782 · user_id from query (spoofable ⚠)
Request · query + body
query: { "user_id" }
body (CreateUsersSavedFilterDto): { "name": "string", "setting": { /* JSONB */ } }
Response
UsersSavedFilterRow (created)
PUT /filters/:id stats.controller.ts:824 · user_id from query (spoofable ⚠)
Request · path + query + body
path: id (UUID)
query: { "user_id" }
body (UpdateUsersSavedFilterDto): { "name?": "string", "setting?": { /* JSONB */ } }
Response
UsersSavedFilterRow | 404
DELETE /filters/:id stats.controller.ts:880 · user_id from query (spoofable ⚠)
Response
{ "deleted": true } | 404

social-analytics · external service — not in this workspace; contract observed via stats-service callers (ANALYTICS_BASE_URL, default http://localhost:3002)

POST /api/import/trigger called by stats-service · stats.service.ts:71
Verified indirectly — stats-service POSTs here when its own /stats/import/trigger is hit
GET /api/charts/* 17 call sites across stats.service.ts
Subpaths observed: /balance, /accounts, /instruments, /holding-period, /top-instruments, /trade-efficiency, /overall-stats, /my-statistics, /roi-daily, /roi-drawdown, /max-open-trades, /profit-calendar/months, /profit-calendar/days, /profit-calendar/day, /trade-pnl-history
GET /api/portfolio/summary called by stats-service · stats.service.ts:2105
Called inside /portfolio/summary on stats-service
↑ Back to overview
Module 08 · Rewards

Rewards

The platform's reward engine for copy-trading. Combines three responsibilities in one service: session lifecycle (from copy start/stop events), periodic reward-job execution (BullMQ workers creating reward transactions, handling overdue/recovery, running month-end settlement), and the payout workflow (wallet + withdrawal modules).

Rewards · session → jobs → payouts RabbitMQ 🐇 startCopy · stopCopy vhost /zulu rewards-service session lifecycle scheduler PostgreSQL sessions · txns Redis + BullMQ reward jobs Background workers create reward transactions overdue / recovery month-end settlement Payout workflow wallet module withdrawal payout-side data management client-express admin-express copy events write enqueue BullMQ triggers payout stats / list

Responsibilities

  • Session lifecycle — creates a copy session when a startCopy event arrives on RabbitMQ; closes it on stopCopy.
  • Reward calculation — schedules recurring reward jobs at configured intervals after session creation.
  • Job execution — background BullMQ workers (Redis-backed) create reward transactions, handle overdue or recovery scenarios, and run month-end settlement so earnings stay consistent even after downtime.
  • Payout workflow — wallet and withdrawal modules own the payout-side data that gets hit after rewards are earned.
  • Public APIs — schedule/stop rewards manually, fetch reward transactions, fetch reward stats, and CRUD-style endpoints on wallet + withdrawal.

Service

rewards-service
NestJs service, backed by PostgreSQL (session + transaction records), Redis + BullMQ (reward jobs), and RabbitMQ (copy-lifecycle intake on the Social Trader vhost /zulu). No external vendor touchpoints — Stripe lives in subscriptions, not here.
🔗 Callers
client-express and admin-express call rewards-service for user-facing reward stats and admin read/management actions. auth-service also references it via the REWARDS_HOST environment variable (used for account-support utilities).
🤝 Boundary with subscriptions
Both subscriptions and rewards-service sit in the billing-adjacent space and both use RabbitMQ + PostgreSQL + Redis/BullMQ. Keep the boundary clean: subscriptions owns entitlements & payment (plan catalogue, Stripe, renewals), rewards owns earnings & payout (copy sessions, transactions, wallet/withdrawal). Copy-lifecycle events pass through rewards, not subscriptions.
↑ Back to overview
Module 09 · Badges

Badges

Gamification engine. Tracks user progress and awards badges when key profile or trading milestones happen. Validates incoming events, maps each topic to a badge rule, updates per-user state, marks badges earned, and writes a history entry for audit. Pure internal — no external vendor touchpoints.

Badges · events → rules → history RabbitMQ 🐇 vhost /zulu · exchange zulu queue INAPP · badges.* badges-service validate · map topic → rule update progress · record PostgreSQL definitions · state · history Redis client-express admin-express Upstream publishers (likely — not traced) auth / users connector-hub verify · account-link · trade-history milestones notifications-service consumes badge.awarded event badges.* write cache HTTP queries badge assigned

Badge event topics

Known topics per the Confluence catalogue. Implementation status per topic not documented here — ignore.

B-MAIL-Vemail verified B-MOB-Vmobile verified B-POR-Vproof of residence B-POI-Vproof of identity B-DEMO-ACCdemo account B-LIVE-ACClive account B-TRADE-HIStrade history B-EAEA

Responsibilities

  • Event intake + validation — pulls messages from badges.*, validates payload structure.
  • Rule mapping — topic → specific badge rule; per-user progress updated in PostgreSQL.
  • Earned marking — when a rule's condition is met, the badge is marked earned for the user.
  • History entry — every change recorded for audit / display.
  • Public APIs — run badge check directly, fetch all active badges (catalogue), fetch user-specific badges with optional account-level context.

Service

badges-service
NestJs. Backed by PostgreSQL (badge definitions, user badge state, badge history), Redis (module wiring / cache), and RabbitMQ (intake on the Social Trader /zulu vhost). No external vendor deps.
🔗 Callers
client-express (via BADGES_SERVICE_URL) and admin-express (per the Confluence admin-linked-services table).
🧭 Upstream publishers (tentative)
The Confluence doc names the topics but not their publishers. Likely owners: auth-service / users-service for the verification events (B-MAIL-V, B-MOB-V, B-POR-V, B-POI-V), connector-hub for account events (B-DEMO-ACC, B-LIVE-ACC), and the persistence / trading pipeline for B-TRADE-HIS. Publishers for B-EA unknown. Flagged as assumption — not traced in code.
🤝 Downstream: notifications-service
When badges-service assigns a badge to a user, it publishes a "badge assigned" event. notifications-service consumes that event and delivers the corresponding notification through the user's configured channels (push, email, SMS, in-app). So the event emitted on badge-award is the handoff between gamification and the notification channel layer.

Architecture improvements

Target-state direction for the badges module. Built from a cross-service scan of badges-service, users-service, connector-hub, auth-service, notifications-service, and client-express — every claim below is backed by a file:line citation. Each tab groups the proposal a different way: the decisions we're committing to, the principles that back them, the concrete risks each decision resolves, and the APIs (HTTP + RMQ) touched.

Implement the badge.assigned publisher
The HTML "Downstream: notifications-service" callout above documents a badge.assigned handoff, but grep across badges-service/app finds zero publish() call for it (only the 8 B-* topics at badges.controller.ts:239-246 — which are inbound topics re-published from the same service). notifications-service/app has no consumer binding either. Publish after every committed badge award; bind on the consumer side.
Fix the nack-on-success path
badges.controller.ts:94 calls nackMessage(ack, false) on the success path — same as the failure paths at :80, :89, :97. Library default is requeue=true (rabbit-mq.service.ts:271); the controller explicitly overrides to false everywhere. Replace the success call with ackMessage(ack). Reserve nackMessage(..., false) for DTO / validation failures only.
Normalise HTTP verbs for badge endpoints
Six per-badge endpoints use @Get but require a request body: /por-verified (:298), /poi-verified (:319), /demo-account (:340), /live-account (:361), /trade-history (:381), /ea-setup (:401). Most HTTP stacks (fetch, curl default, some proxies) strip GET bodies. Convert to @Post for parity with /email-verified (:252) and /mobile-verified (:277).
Fix the POI copy-paste message
badges.controller.ts:319 returns { message: 'POR badge logic executed' } on the POI success response — literal copied from the POR handler. Minor, but surfaces in clients.
Remove (or relocate) the self-publish block
badges.controller.ts:239-246 publishes every B-* topic from inside the same service that consumes them (8 calls to this.rabbit.publish(..., 'B-MAIL-V') ... 'B-EA'). Keeps the controller as its own upstream producer. Move to test/ or delete — legitimate producers live in users-service and connector-hub.
Correct the HTML queue label
The Badges SVG at zulutrade-architecture.html labels the queue INAPP · badges.*. The runtime queue is badges (badges.controller.ts:49). INAPP is an internal persistence buffer inside the RMQ library (rabbit-mq.service.ts:36), not the user-facing queue name.
Trace or remove the orphan topics
B-POR-V, B-POI-V, and B-EA are bound as consumer topics (badges.controller.ts:34-39) but grep across users-service, auth-service, connector-hub, and client-express finds no publisher. Either implement the publishers (POR / POI during KYC; EA on activation) or drop the topics from the catalogue until they're wired.
P1 Publishers exist in code, not just in diagrams
Every documented event has a publish() call in a named producer. No ghost publishers (today: badge.assigned). Contract tests on both sides so drift fails in CI.
P2 ACK success · NACK failure · DLQ poison
Message-processing outcomes map to specific broker actions: success → ack; transient failure → nack requeue; permanent / poison → nack no-requeue (eventually DLQ). "Nack everything" (today's pattern at badges.controller.ts:94) is none of these and destroys observability.
P3 HTTP verbs match intent
State-changing endpoints use POST / PUT / DELETE. GET carries no body. Applies to the six @Get-with-body endpoints in badges.controller.ts.
IDRisk todayResolved by
R1HTML "Downstream: notifications-service" callout documents a badge.assigned handoff; no publisher exists in badges-service and no consumer in notifications-service. Users earn badges silently.Implement the publisher after each badge-award commit; add consumer binding in notifications-service; contract test both sides.
R2badges.controller.ts:94 nacks with requeue=false on the success path — identical broker outcome to failures at :80, :89, :97. No observable difference between a processed message and a rejected one.ack on success; nack-requeue on transient fail; nack-no-requeue + DLQ on permanent fail.
R3Six @Get badge endpoints expect a request body — /por-verified, /poi-verified, /demo-account, /live-account, /trade-history, /ea-setup. Many HTTP stacks silently strip GET bodies.Convert to @Post (parity with /email-verified and /mobile-verified).
R4badges.controller.ts:319 returns 'POR badge logic executed' in the POI success response (copy-paste bug).Return the correct literal.
R5HTML SVG labels the queue INAPP (zulutrade-architecture.html). Code binds queue badges (badges.controller.ts:49). INAPP is an RMQ-lib internal buffer.Update the SVG label to badges; document the internal tempQueue separately if needed.
R6badges.controller.ts:239-246 publishes all 8 B-* topics from inside the consumer service — the controller is both upstream producer and downstream consumer for the same topics.Delete the block or move to test/. Production producers live in users-service and connector-hub.
R73 of the 8 consumer bindings (B-POR-V, B-POI-V, B-EAbadges.controller.ts:34-39) have no publisher in any scanned service. Consumers wait forever.Trace / implement publishers (POR + POI during KYC; EA at activation), or drop the topics from the catalogue until wired.
R8HTML "Upstream publishers (tentative)" callout attributes B-TRADE-HIS to "the persistence / trading pipeline". Actual publisher is connector-hub/app/express/plugins/copy-trading/handler-remote/index.ts:463.Update the HTML attribution; add a contract test on the connector-hub side.
R9HTML "Upstream publishers (tentative)" lists "auth-service / users-service" as joint owners of B-MAIL-V and B-MOB-V. auth-service publishes zero badge topics (grep confirmed); users-service owns both (routes/index.ts:78, handler-remote/index.ts:634).Narrow the HTML attribution to users-service.

Click an endpoint to expand its request & response. Fields shown are those cited in api_docs/badges-service.md and the controller source. Envelope: { httpStatus, httpCode, message, data } on success; { httpStatus, httpCode, errorCode, message, errors } on error (app/core/response-handler/response-handler.service.ts:14,34).

zulu3.0-badges-service · HTTP triggers mirroring the 8 B-* topics + catalogue + per-user read

POST /email-verified/:userId badges-service · badges.controller.ts:252
DTO: EmailVerifiedPayload (dto/badges.dto.ts:62) Auth: no @UseGuards in-service
Request
Path:   { userId: number }
Body:   { email: string }   // @IsEmail, default ''
Response · 200
{ data: { message: 'Email badge logic executed' } }
POST /mobile-verified/:userId badges-service · badges.controller.ts:277
DTO: PhoneVerifiedPayload (dto/badges.dto.ts:67)
Request
Path:   { userId: number }
Body:   { mobile: string }   // length 10–15
Response · 200
{ data: { message: 'Mobile badge logic executed' } }
GET /por-verified/:userId badges-service · badges.controller.ts:298
Verb-body quirk: decorator is @Get but handler expects a body. See R3. DTO: PORVerifiedPayload (dto/badges.dto.ts:73)
Request
Path:   { userId: number }
Body:   { por_number: string }
Response · 200
{ data: { message: 'POR badge logic executed' } }
GET /poi-verified/:userId badges-service · badges.controller.ts:319
Verb-body quirk: @Get with body (R3). Copy-paste bug: response message literal is 'POR badge logic executed' (R4). DTO: POIVerifiedPayload (dto/badges.dto.ts:78)
Request
Path:   { userId: number }
Body:   { poi_number: string }
Response · 200
{ data: { message: 'POR badge logic executed' } }   // sic
GET /demo-account/:userId badges-service · badges.controller.ts:340
Verb-body quirk: @Get with body (R3). DTO: DemoAccountPayload (dto/badges.dto.ts:83)
Request
Path:   { userId: number }
Body:   {
  algogems_account_address: string,
  leader_uuid: string,
  leader_display_name: string,
  account_id?: number   // default 0
}
Response · 200
{ data: { message: 'Demo Account badge logic executed' } }
GET /live-account/:userId badges-service · badges.controller.ts:361
Verb-body quirk: @Get with body (R3). DTO: LiveAccountPayload (dto/badges.dto.ts:102) — same shape as DemoAccountPayload.
Response · 200
{ data: { message: 'Live Account badge logic executed' } }
GET /trade-history/:userId badges-service · badges.controller.ts:381
Verb-body quirk: @Get with body (R3). DTO: TradeHistoryPayload (dto/badges.dto.ts:103) — same shape as DemoAccountPayload.
Response · 200
{ data: { message: 'Trade History badge logic executed' } }
GET /ea-setup/:userId badges-service · badges.controller.ts:401
Verb-body quirk: @Get with body (R3). DTO: EAPayload (dto/badges.dto.ts:104) — same shape as DemoAccountPayload.
Response · 200
{ data: { message: 'EA Setup badge logic executed' } }
GET / badges-service · badges.controller.ts:422
Purpose: global catalogue of active badges
Response · 200
{ data: Badge[] }   // shape returned by badgesService.getAllActiveBadges()
Response · 500
{ errorCode: 'BADGE_FETCH_ERROR' }
GET /user/:userId badges-service · badges.controller.ts:438
Purpose: per-user badge aggregation, optional scoping to one trading account
Request
Path:   { userId: number }
Query:  { algogems_account_address?: string }
Response · 200
{ data: UserBadges }   // shape from badgesService.getUserBadges()
Response · 500
{ errorCode: 'BADGE_FETCH_ERROR' }

Async handoffs · B-* topics (publishers → badges-service) · queue badges, prefetch 50 (badges.controller.ts:58)

EVENT 🐇 B-MAIL-V users-service · routes/index.ts:78 → badges-service
Publisher (confirmed): users-serviceroutes/index.ts:78 DTO: EmailVerifiedPayload
EVENT 🐇 B-MOB-V users-service · handler-remote/index.ts:634 → badges-service
Publisher (confirmed): users-servicehandler-remote/index.ts:634 DTO: PhoneVerifiedPayload
EVENT 🐇 B-TRADE-HIS connector-hub · copy-trading/handler-remote/index.ts:463 → badges-service
Publisher (confirmed): connector-hubcopy-trading/handler-remote/index.ts:463 HTML drift: HTML "Upstream publishers" callout attributes this to "the persistence / trading pipeline". See R8. DTO: TradeHistoryPayload
EVENT 🐇 B-LIVE-ACC connector-hub · copy-trading/handler-remote/index.ts:469 → badges-service
Publisher (confirmed): connector-hubcopy-trading/handler-remote/index.ts:469 DTO: LiveAccountPayload
EVENT 🐇 B-DEMO-ACC connector-hub · copy-trading/handler-remote/index.ts:471 → badges-service
Publisher (confirmed): connector-hubcopy-trading/handler-remote/index.ts:471 DTO: DemoAccountPayload
ORPHAN 🐇 B-POR-V consumer wired — no publisher found
Consumer binding: badges.controller.ts:34 Publisher: not found in users-service, auth-service, connector-hub, client-express Impact: POR-verified badge can only be fired via the HTTP endpoint (:298). RMQ-initiated flow is dead until a publisher lands. See R7.
ORPHAN 🐇 B-POI-V consumer wired — no publisher found
Consumer binding: badges.controller.ts:35 Publisher: not found in scanned services. See R7.
ORPHAN 🐇 B-EA consumer wired — no publisher found
Consumer binding: badges.controller.ts:39 Publisher: not found in scanned services. See R7.
MISSING 🐇 badge.assigned badges-service → notifications-service · not implemented
HTML claim: "Downstream: notifications-service" callout (above) documents this handoff. Code reality: no publish() call for badge.assigned in badges-service/app; no consumer binding in notifications-service/app. See R1.
↑ Back to overview
Module 10 · Application Layer

Other Services

A horizontal band of single-purpose services that support the product surface — notifications, billing, gamification, community, and analytics. All of them sit behind client-express and share the zulu3 Postgres database.

🔗 External integrations
Stripe Mastodon DeepL

Services

notification-service
In-app and push notifications. Fan-out from RabbitMQ events (new follower, order filled, copy stopped, etc.).
subscription-service
Subscription plans & entitlements. Integrates with Stripe for billing and recurring payments.
rewards-service
Promotions, referral credits, and cashback logic.
badges-service
Gamification — awards badges based on trading milestones, tenure, and social behaviour.
community-connector
Bridge to the self-hosted Mastodon instance for social feed, follows, and posts.
stats-service
Per-user and per-leader trading stats (win rate, ROI, drawdown, follower counts).
social-analytics
Cross-platform analytics aggregator. Consumes signals from stats-service and the social bridge layer.

External Integrations

ServiceProviderPurposeProtocol
subscription-serviceStripeBilling & recurring paymentsHTTPS / Webhooks
communicationSendgridTransactional emailHTTPS
auth-serviceApple · GoogleFederated SSOOAuth 2.0
community-connectorMastodon (self-hosted)Social feed & followsREST · ActivityPub
↑ Back to overview
Module 11 · Admin Surface

Admin

The operator-facing side of the platform — a thin gateway (admin-express) fronts all CRM, support, and operational workflows, backed by a MariaDB-only DAL and a dedicated roles/permissions service. The admin tier reuses most of the client-tier backend rather than duplicating logic.

Admin · gateway + shared backend admin-express admin-mysql admin-service client-mysql Reused CRM backend auth-service users-service connector-hub communication temp-traders badges-service notifications-svc rewards-service subscriptions

Admin-owned services

admin-express
Admin HTTP gateway. Per the Confluence interlinking graph it calls 12 backend services — admin-mysql, client-mysql, admin-service (roles/permissions/teams), auth-service, badges-service, connector-hub, communication, notifications-service, rewards-service, subscriptions (subscription data), users-service (user list + detail), and temp-traders (trade history).
admin-mysql
MariaDB-backed DAL for the admin surface. Handles auth and verifies users and admins; HTTP facade over the same MariaDB host the client-tier DAL uses.
admin-service
Admin domain logic — roles, permissions, and teams. Called by admin-express whenever an operator action needs an authorization / access-control check.
🔗 Admin reuses the client-tier backend
The admin tier owns only three services. All CRM functionality (users, auth, badges, rewards, subscriptions, notifications, communication, connectors, temp-trader history) is served by the same backend that powers the client surface. Admin only layers roles/permissions on top via admin-service.
↑ Back to overview
Module 12 · Ops & Data Migration

Ops / Data Pipeline

Background / ops-only services that don't sit on any request path. data-pipeline and data-pipeline-setup run data-migration workloads; import-history backfills historical trading data into the analytics store. These are tooling and plumbing, not product features.

Services

data-pipeline
Bash-driven backend data-migration pipeline. Moves/transforms records between stores during cutover or catch-up windows.
data-pipeline-setup
Bash-driven companion to data-pipeline. Prepares environment, schema, and source/target connectivity before the main pipeline runs.
import-history
NestJs analytics-tagged service. Backfills historical trading data so the analytics layer (used by stats-service) has complete coverage. Referenced by stats-service via the analytics-importer trigger path.
🛠 Out-of-band
None of these three sit on the user's request path. They're invoked by operators or scheduled jobs. Treat them as tooling that's part of the platform but not part of the live traffic graph.
↑ Back to overview
Module 13 · Infrastructure

Data & Infrastructure

Storage choices are deliberate. Every datastore has a single job and a clear owner.

StoreTypeOwnerPurpose
zulu3_biz, zulu3_session MariaDB client-mysql Legacy — login & registration parity only.
zulu3_communication MariaDB communication Email templates, delivery logs, bounce tracking.
zulu3 PostgreSQL users-service · auth-service · most app services Canonical relational store for users, subscriptions, leaders, copiers, badges, rewards.
social-bridge PostgreSQL node-middleware Canonical relational store for account, trade actions & symbol config from terminals.
mastodon PostgreSQL community-connector Backing store for the self-hosted Mastodon instance (social feed).
redis-stack Redis Trading Flow services Hot state for the signal pipeline. Also used by bull-board for job queues.
clickhouse Clickhouse social-analytics · persistence-controller Column-store for analytical workloads — stats, leaderboards, time-series queries.

Operational Tooling

bull-board
UI on top of BullMQ / Redis queues. Used by ops for inspecting and retrying background jobs.
RabbitMQ 🐇
Event bus. Every inter-service async message goes here. Topic exchanges per domain.
↑ Back to overview
Reference · Runtime

Inventory

Every service declared in the project's docker-compose.yml, grouped by role, with runtime status (2026-04-20 snapshot). Hosts are named only by their canonical role.

Onboarding · Login

ServiceRoleStatusNotes
client-expressPublic HTTP gateway (client)upConsumes MariaDB, Redis, MongoDB (broken), DeepL, FXView pricing.
admin-expressAdmin HTTP gatewayupSame stores as client-express.
users-serviceCanonical user identityupOwns user records.
auth-serviceSessions / JWT, SSOupApple + Google OAuth.
client-mysqlNode DAL (not a DB)upHTTP facade over MariaDB.
admin-mysqlNode DAL (not a DB)upHTTP facade over MariaDB.
communicationTransactional emailupDispatches via Sendgrid.
admin-serviceAdmin domain logicup
flavours-serviceFeature flags / white-label configup

Account Connection

ServiceRoleStatusNotes
connector-hubCentral orchestratorupTalks to ActTrader, FTL, FXView; subscribes to multiple external RabbitMQ brokers.
temp-traderDemo accounts → S281up
leaders-serviceLeader profilesupIntegrates with FTL.
copier-serviceCopier subscriptionsupIntegrates with FTL.
subscriptionsBilling & subscription lifecycleupStripe integration.
community-connectorMastodon bridgeupSingle target — Mastodon instance.

Engagement

ServiceRoleStatusNotes
notifications-serviceIn-app / push fan-outupConsumes RabbitMQ events.
rewards-servicePromotions, referral creditsup
badges-serviceGamification badgesup
stats-servicePer-user / per-leader statsupUses external analytics source.

Analytics

ServiceRoleStatusNotes
auth-service · also in OnboardingExports plugin · ACT-281 → filesupPlugin exports trades from ACT-281 to disk; to be extracted into its own service in a later phase.
social-analyticsFile ingest → ClickHouse writerupConsumes the exported files and populates the ClickHouse analytics store.
stats-service · also in EngagementAnalytics API for client/admin UIsupReads analytics via social-analytics (not ClickHouse directly).

Rewards

ServiceRoleStatusNotes
rewards-service · also in EngagementCopy-trading reward engineupSession lifecycle on startCopy/stopCopy, BullMQ jobs, month-end settlement, wallet + withdrawal.

Badges

ServiceRoleStatusNotes
badges-service · also in EngagementGamification · milestones → historyupConsumes badges.* (B-MAIL-V, B-MOB-V, B-POR-V, B-POI-V, B-DEMO-ACC, B-LIVE-ACC, B-TRADE-HIS, B-EA) on vhost /zulu.

Trading Flow

ServiceRoleStatusNotes
signal-junctionInbound signal entry + fan-outupSplits faulty from clean signals.
signal-synchronizerSignal repair / synchronizationupRepair shop for malformed signals.
signal-processorCore signal processingupApplies copy rules, risk caps, leader-copier fan-out.
act-signal-processorACT-specific signal transformerupNormalises to ACT broker order format.
act-bridge-signal-processorACT return-path handlerupProcesses fills back into the pipeline.
signal-outFinal router to execution venueup
persistence-servicePersistence / ClickHouse writerupOff-loads hot-path writes.

Bridges

ACT Bridge

ServiceRoleStatusNotes
act-bridgeAdapter to ACT brokersupSpeaks ACT native protocol.
order-updateWebSocket consumer for broker eventsupFills, partial fills, rejects; feeds back into the trading-flow pipeline.

node-middleware

ServiceRoleStatusNotes
node-middlewareBridge to terminal platforms (MT4, MT5, cTrader, Discord, Telegram)upSits between the signal pipeline and the actual trading terminals. Handles each platform's native protocol.

Admin

ServiceRoleStatusNotes
admin-expressAdmin HTTP gatewayupOrchestrates 12 backend services for CRM, support, and operations.
admin-mysqlNode DAL (not a DB)upMariaDB-backed DAL. Handles auth and verifies users and admins.
admin-serviceRoles / permissions / teamsupAccess-control domain logic.

Ops / Data Pipeline

ServiceRoleStatusNotes
data-pipelineBackend data migrationopsBash. Data migration pipeline; runs during cutover / catch-up windows.
data-pipeline-setupMigration environment setupopsBash. Companion to data-pipeline — prepares schema + connectivity.
import-historyHistorical analytics backfillopsNestJs. Referenced by stats-service via the analytics-importer trigger.

Infrastructure

ComponentLocationStatusNotes
MariaDBApplication Server (host process)upReplicates with separate peer machines.
PostgreSQLApplication ServerupSymbol-mapping db used by connector-hub.
RedisApplication Serverupredis-stack image.
RabbitMQ (on-host)Application Serveridle0 active connections — awaiting consolidation.
RabbitMQ (Social Trader legacy)external (legacy platform)primaryvhost /zulu — primary bus today.
RabbitMQ (ACT 281 broker)external (ACT platform)activevhost /act — order / fill events.
MongoDBApplication Server⚠ crash-loopAVX-less host — mongo:latest incompatible.
ClickHouseexternal analytics platformupAnalytical sink for stats-service and persistence-service.
↑ Back to overview