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.
Click any module to jump to its detailed section below. Each section lists its external integrations.
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.
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.
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.
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.
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
zulu3_biz, zulu3_session). Used only for login and legacy registration compatibility.zulu3 Postgres. All user lookups go here post-onboarding.zulu3_communication and dispatches via Sendgrid.Sign-up Sequence — Email + OTP
- Register request — FE submits the registration form to
client-express. - Lead created —
client-expresswrites a lead record into MariaDB viaclient-mysql. - OTP email —
client-expresstriggerscommunicationservice, which dispatches the OTP email through Sendgrid. - OTP verified — user submits the OTP;
client-express→client-mysqlcreates the user record in MariaDB. This record is later used byclient-expressto issue JWT tokens and manage login sessions / auth. - Postgres mirror —
client-expresssends a POST tousers-service, which creates the matching user record in the Postgreszulu3database. - Event broadcast —
users-servicepublishes auserRegisterevent on RabbitMQ so every downstream service can react. - ACT customer profile —
connector-hublistens to theuserRegisterevent and passes it on totemp-trader, which creates an ACT customer profile. On successful profile creation, anActCustomerevent is published on RabbitMQ;users-serviceconsumes it and stores the details in the Postgres user profile.
Sign-up Sequence — SSO (Google / Apple)
client-expressforwards the SSO request directly toauth-service.- The provider verifies email ownership. On success,
client-expresscreates 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.
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.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.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.client-express and downstream services have a single config source. All FE access routes through the gateway, not direct to the service.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.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.userRegister / ACTCustomer dedups on eventId via Redis (24 h TTL). Redeliveries become safe; duplicate publishes become safe. One shared helper, used everywhere.zulu3. No cross-schema writes. Cross-service reads via published views or read-only APIs. Stops the distributed-monolith slide.zulu3. MariaDB is a read-replica during transition. JWT reads the same row the writer just wrote. No distributed transactions, no dual-write race.
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.
eventId. Every consumer dedups. Redeliveries, duplicate publishes, and retries become safe by construction — we don't chase exactly-once at the infra level.
client-mysql should not host 14 domain plugins. A service called auth-service should not export trade history. Truth-in-names, or rename.
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.
ACTCustomer mislabelling is the kind of bug this makes impossible.
| ID | Risk today | Resolved by |
|---|---|---|
| R1 | Dual 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. |
| R2 | client-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. |
| R3 | client-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. |
| R4 | auth-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. |
| R5 | temp-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. |
| R6 | No 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. |
| R7 | Shared 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. |
| R8 | Sendgrid 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. |
| R9 | FE 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
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
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
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
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
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
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
Request · query
?state=<csrf-state>
Response · 200
{ "url": "https://appleid.apple.com/auth/authorize?..." }
POST /apple/redirect auth-service · apple-auth.controller.ts:30
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
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
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
Request · body
{
"requestId": "uuid",
"otp": "string" // 4-6 digits
}
Response · 200
{ "data": { "verified": true } }
POST /verification/email/request auth-service · verification.controller.ts:39
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
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
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
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
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
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
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
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
Payload
{
"requestId": "uuid",
"userId": 123456,
"email": "alice@example.com",
"otp": "123456",
"templateId": "OTP_REGISTRATION"
}
EVENT 🐇 B-MAIL-V users-service → badges-service
Payload
{
"userId": 123456,
"email": "alice@example.com",
"verifiedAt": "ISO-8601"
}
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.
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.
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/false → true/false → true/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.
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).
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 request — connector-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.
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 session — act-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.
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.
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
sessions queue, and triggers the mirror-account + Leader/Copier wiring.sessions queue.ActCustomer events.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.
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.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.https://conversion.fxview.com/api/get/price — queue-listners/info.ts:106-107), CP-Core (CP_CORE_BASEPATH — libs/cp-core-helper/config/config.ts:5), and node-middleware (MIDDLEWARE_BASE_URL — brokers/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.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).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.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.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.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.ACTCustomer consumer in users-serviceusers-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).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.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.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).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.connector-hub's 10 queues belongs to exactly one plugin (act, copy-trading, symbol-mapping, queue-listners, …) — no cross-plugin consumer routing.
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.
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.
ACTCustomer in users-service) dedups on eventId via Redis (24 h TTL). Redeliveries and duplicate publishes are safe by construction.
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.
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.
| ID | Risk today | Resolved by |
|---|---|---|
| R1 | connector-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. |
| R2 | The HTML documented subscription-end as the connector-hub consumer topic; code actually binds subscription-cancel → cancelSubs (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. |
| R3 | FXView (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. |
| R4 | temp-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. |
| R5 | leaders-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. |
| R6 | The 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. |
| R7 | Hardcoded 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. |
| R8 | leaders-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. |
| R9 | users-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. |
| R10 | The 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. |
| R11 | MT / 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. |
| R12 | Token-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). |
| R13 | Mount / 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
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
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
Request · body
{
"username": "string?",
"email": "from auth"
// additional fields per ACTValidators.createDemoAccountV2()
}
POST /leader/create connector-hub · copy-trading/routes/index.ts:28
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
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
Request · query
?leader_uuid=<uuid>&algogems_account_address=<addr>&closeAccount=<bool>
GET /v2/platforms connector-hub · brokers/routes/index.ts:67
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
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
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
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
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
Async handoffs on the account-connection path · not HTTP — RabbitMQ events
EVENT 🐇 ACTCustomer (consumer) users-service · handler-remote/index.ts:25, 65
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
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
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
Payload
Raw inbound text message from Telegram / Discord; shape varies by platform adapter.
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.
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 path —
subscriptionsexposes 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
subscriptionsprocesses 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:
- Calls
copier-serviceto create the follower record — forwarded to FTL. - Publishes a
startCopyevent on RabbitMQ (Social Trader broker, vhost/zulu, exchangezulu). rewards-serviceconsumesstartCopy, creates a copy session in PostgreSQL, and schedules recurring reward jobs via BullMQ. Detail in Module 08 Rewards.notifications-servicesends 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)
- User stops from UI →
connector-hub→copier-serviceremoves the follower record on FTL. connector-hubpublishesstopCopyon RabbitMQ.rewards-servicecloses 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.- The subscription itself continues unless the user cancels / pauses separately via
subscriptionsAPIs. - If the subscription ends, copying also stops —
subscriptionspublishes the subscription-end event;connector-hubreacts by triggeringstopCopyfor the affected follower records.
Services touched in this lifecycle
| Service | Role in Copier Flow | Primary module |
|---|---|---|
connector-hub | Orchestrator (calls copier-service, publishes startCopy/stopCopy) | 02 |
copier-service | Follower CRUD → FTL | 02 |
subscriptions | Plan / cart / activation / renewal (Stripe) | inline here · breadcrumb in 10 |
rewards-service | Copy-session bootstrap on startCopy; session close on stopCopy | 08 |
notifications-service | User confirmations | 10 |
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.
admin-express + admin-service. No subscription is required — Leader participation is free-to-register and earning-based.
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
- On approval,
connector-hubcallsleaders-serviceto create the Leader record.leaders-serviceowns no state — it forwards to FTL, which holds the actual Leader record. connector-hubhandles leader / watchlist / strategy management (symbols, marketing description, copy settings — exact scope depends on how "strategy" is configured in the platform).notifications-servicesends 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-hub→leaders-service(remove) → FTL. - Admin-revoked — admin uses
admin-expressto revoke the Leader → same downstream path.
Services touched in this lifecycle
| Service | Role in Leader Flow | Primary module |
|---|---|---|
client-express | Application submission from user UI | 01 |
auth-service | Session + SSO | 01 |
users-service | Profile | 01 |
connector-hub | Orchestrator (calls leaders-service, owns strategy management) | 02 |
leaders-service | Leader CRUD → FTL | 02 |
temp-trader | ACT-side broker integration | 02 |
admin-express | Admin review + approval gateway | 11 |
admin-service | Roles / permissions / approval audit | 11 |
rewards-service | Accrues Leader earnings from copier activity | 08 |
notifications-service | Approval / rejection / removal notifications | 10 |
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.
copier-service and leaders-service into one service. 95% duplication with active drift (ClientFactory.put() only in copier-service) is the decider.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.ParticipantRoute / ParticipantRemote in _participants-base/; per-role files only declare config + schema. Update/create/list/remove are inherited, not copy-pasted.handler-remote is the only side that talks to FTL; handler-local is this service's own state only.participant.leader.* / participant.copier.* so signal-junction, order-update, persistence-service can react.PUT /participants/:role/:uuid for every role by default."temp" token (used on every FTL call in both services today) with an env-injected service token validated on the FTL side.shared/validator/index.ts schemas are empty — unvalidated payloads forwarded to FTL. Validator becomes a required constructor arg of ParticipantRoute.handler-local for this service's own state, handler-remote for FTL — never mix. Prevents the gateway from silently becoming a business-logic service.x-request-id to FTL; event envelope carries it to consumers.| Risk today | How 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 capability | Generic base exposes update for every role by default |
Hardcoded "temp" auth token | One http-client interceptor injects real service token for all roles |
| Unused RabbitMQ → downstream services starve for events | ParticipantEvents publisher is in the hot path, can't be skipped |
| Double deploy / monitoring overhead for trivial logic | One image, one alert set, one dashboard |
| Future role (e.g. institutional copier) = new repo | New role = new plugin dir + config file |
| Empty validators = unvalidated proxy | Validator is a required constructor arg of ParticipantRoute |
| Correlation-id lost at the FTL hop | Axios 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
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
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
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
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
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
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
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
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
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 toconnector-huborchestration logic, just URL + token. - Module 04 Leader Flow — Phase 5 registration and Phase 6 closeout retarget to
/participants/leaders/*. Phase 5 also gains the missingPUTfor strategy updates (previously had to go throughconnector-hub-only paths). - Module 05 Trading Flow and Module 10 Other Services — new
participant.*events become consumable bysignal-junction,order-update,persistence-service,notifications-service.
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.
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.
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.
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.
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
- Ingestion — MT4/MT5 and TG/Discord orders enter via
Node Middleware. Web orders (Demo & Act accounts) enter viaAct web trader System. ACT platform and Fxview s245 broker events enter viaOrder Update serviceover WebSocket. - Classification —
Signal junctionroutes each MT/Social signal to a platform-specific queue (MT4,MT5,TG_CONNECTOR,DISCORD). Defect detection (price=0 for MT4/MT5/CTrade) writes adefect:<algo>_<order>key to Redis for the externalsignal-synchronizerto repair. - Processing —
Signal Processor(ts-zulu3.0-signal-processor) consumes the platform queues and the sharedACTqueue. It calls FTL s281 via HTTP (leader/copier/orderURLs) to execute leader-copier fan-out and publishessignal_orders+trade_logsto the persistence pipeline. - Return / Routing — FTL pushes resulting signals over WebSocket to
act-signal-processor, which publishesftl-signalon theftlqueue.Signal Outconsumes it and dispatches via platform publishers: MT copiers → routing keyoutonsignal.outqueue (consumed by Node Middleware → terminal); Demo/Act/TG/Discord copiers → routing keyACTon theACTqueue (looped back into Signal Processor); ACT broker dispatch →act signal outqueue →Act Bridge Service. - Web-order ack path — For orders placed via Zulu Web,
Signal Outalso publishes an immediate acknowledgement on routing keyOU<AccountID>(Order Update ack) and an RPC reply to the caller's reply queue, then ACKs the web-orders message last. - ACT execution feedback — ACT broker dealer sockets push
trade/orderevents toOrder Update serviceover WebSocket. Order Update republishes on thesignalDataexchange (routing keyorder).Act-bridge-signal-processorconsumes theact.orderqueue, normalises, and publishes back into Signal Processor via theACTqueue to keep state coherent. - Persistence —
Persistence servicebinds thepersistencequeue to thesignal_ordersandtrade_logstopics 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
defect:* key for the external signal-synchronizer to repair.signal_orders and trade_logs for persistence. Invalid or duplicate signals are rejected safely with traceable logs.ftl-signal (copy signals), ftl-execution, ftl-order, unregister — so Signal Processor and Signal Out react in real time.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.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 topic on signalData, and deposit/withdraw events to DPT_WDL on rabbit-social.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.
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.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.act-bridge-signal-processor keeps its normalising role but drops direct PG writes, persistence publishing, web.order.update emission, and the ACT-loopback.act-bridge becomes a pure outbound HTTP adapter. No PG writes, no persistence publish. Token fetch gets a distributed lock.web-orders and ftl-signal.ui-projector and notification-projector subscribe to trade.events and own web.order.update and notify as single writers.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.signal_processor.signals and signal_processor.trade_logs. No more direct PG writes from other services.signal_orders, web.order.update, ACT) are the root cause of the cyclic loop and dedup hazards.
PlaceOrderCommand) go to a specific service; events (OrderPlaced) are past-tense, broadcast facts. The ACT queue today mixes both — that is why it loops.
trade_id everywherepersistence-service writes PG. act-bridge and act-bridge-signal-processor stop writing directly. Redis is the live state; PG is the async shadow.
| ID | Risk today | Resolved by |
|---|---|---|
| R1 | Cyclic ACT loop — 3 publishers, 1 consumer, re-entry unclear | ACT queue becomes ingress-only (single writer: act-normalizer); feedback flows via trade.events. No topic is both ingress and feedback. |
| R2 | Dual consumers on web-orders + ftl-signal cause non-deterministic processing | Delete legacy path. One controller per queue. Feature-flag cutover. |
| R3 | Hot-path PG writes from act-bridge + act-bridge-signal-processor | All PG writes go through persistence-service only. |
| R4 | RPC over RMQ on web-orders (30 s expiration) with dual-write risk | Direct REST endpoint on trading-engine for web orders; event emission for downstream. No self-loop. |
| R5 | No correlation id — debugging a trade requires grep across 8 log streams | Canonical envelope with trade_id / event_id; enforced at ingress; logged everywhere. |
| R6 | web.order.update and notify emitted by multiple services with no ownership | Projections own these as single writers (ui-projector, notification-projector). |
| R7 | Redis / PG consistency unverified; "rebuild from PG" asserted but untested | persistence-service is the single writer; Redis treated as disposable; rebuild drill scheduled. |
| R8 | Two near-identical WS relays (order-update, act-signal-processor) | Merge into broker-ingress with per-driver config. |
| R9 | Token fetch race inside act-bridge | Distributed lock (Redis SETNX) around token refresh; single-flight per (platform, account). |
| R10 | Defect / repair loop has no alert — stuck signal-synchronizer silently drops when 24 h TTL expires | Keep 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
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
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
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
Request · body
{ "userEmail": "string", "order_id": "string" }
Response
{ "success": true, "data": { "orderType": "cancel_order" } }
POST /place/stop act-bridge.controller.ts:246
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
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
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
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 */ } }
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.
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
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) — 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.
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
| Service | Role | Status | Notes |
|---|---|---|---|
act-bridge | Adapter to ACT brokers | up | Speaks ACT native protocol; outbound over HTTP using cached broker session tokens. |
order-update | WebSocket consumer for broker order events | up | Fills, partial fills, rejects; feeds back into the trading-flow pipeline via act-bridge-signal-processor. |
Social Bridge
| Service | Role | Status | Notes |
|---|---|---|---|
Social Bridge — Node middleware | Terminal-ecosystem bridge | up | Separate 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. |
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.
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
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.@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
RabbitMQ queues used by Social Bridge
| Queue | Direction | Purpose |
|---|---|---|
SIGNAL_OUT | consume | Main intake — outbound signals from signal-out that need to land on a terminal. |
SIGNAL_IN | publish | Inbound signals originating from terminal events (e.g., a Telegram message or a Discord command) pushed back into the trading pipeline. |
SIGNAL_VERIFICATION | consume / publish | 2FA / verification codes moving between the connector that needs them and whichever service can fulfil them. |
AI_MESSAGE | consume | Messages to be parsed by the OpenAI integration (see 2FA extraction callout above). |
SESSIONS | consume / publish | Session lifecycle events — connector boot, auth, disconnect. |
SIGNAL_INFO | consume / publish | Signal metadata / info messages paralleling the main signal flow. |
ERROR | publish | Structured 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/*login, logout, send-signal. Auth policy: auth-copy-trading-core./api/admin/v1/*auth-admin-portal.ws://… (connectors over WebSocket)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.
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.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.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.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.'kfjNr@@CNINmPiwgp' on line 26. Move this to a shared @stackflow-bootstrap package and remove the fallback entirely.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.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.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.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.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.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.app.module.ts / configs. Fail loud on startup if an env var is missing — silent fallbacks hide config drift across environments.| Risk today | How 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 flow | Merge 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 route | Use 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 expiry | Short-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 deploys | Implement 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 story | Either wire it up or delete it; no "ghost architecture" in imports |
Existing HTML claimed /api/connectors/v1/* was HTTP — actually WebSocket | Surface corrected in this module; connectors labeled ws://… with auth mechanism called out |
| No correlation-id across ingestion → rabbit → execution hops | HTTP/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
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
Request · body
{ /* PlaceOrderPayload */ }
Response · 202
{ "status": "queued", "correlationId": "…" }
POST /place/limit act-bridge.controller.ts:302 · async → order.place.limit
Request · body
{ /* PlaceLimitPayload */ }POST /place/stop act-bridge.controller.ts:246 · async → order.place.stop
Request · body
{ /* PlaceStopPayload */ }POST /place/trial act-bridge.controller.ts:274 · async → order.place.trail
Request · body
{ /* PlaceTrailPayload */ }POST /modify/order act-bridge.controller.ts:190 · async → order.modify
Request · body
{ /* ModifyOrderPayload */ }POST /cancel/order act-bridge.controller.ts:218 · async → order.cancel
Request · body
{ /* CancelOrderPayload */ }POST /close/trade act-bridge.controller.ts:332 · sync → ACT API
Request · body
{ /* CloseTradePayload */ }POST /trade/place/limit act-bridge.controller.ts:351
POST /trade/place/stop act-bridge.controller.ts:379
POST /trade/place/trail act-bridge.controller.ts:407
POST /open/trades act-bridge.controller.ts:435 · sync → ACT API
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
POST /act-trader/customer/trader/create act-trader.controller.ts:86
POST /act-trader/customer/trader/modify act-trader.controller.ts:110
POST /act-trader/self/trader/create act-trader.controller.ts:133
POST /act-trader/account/create act-trader.controller.ts:157
POST /act-trader/account/modify act-trader.controller.ts:180
GET /act-trader/account/get act-trader.controller.ts:203
POST /act-trader/act/change/password act-trader.controller.ts:227
POST /act-trader/change/act/leverage act-trader.controller.ts:250
POST /act-trader/deposit/balance act-trader.controller.ts:274
POST /act-trader/withdraw/amount act-trader.controller.ts:297
POST /act-trader/adjustment/balance act-trader.controller.ts:320
POST /act-trader/change/balance act-trader.controller.ts:343
POST /act-trader/change/bonus act-trader.controller.ts:367
POST /act-trader/get/margin act-trader.controller.ts:391
POST /act-trader/deposit/history act-trader.controller.ts:415
POST /act-trader/account/history act-trader.controller.ts:438
POST /act-trader/get/doc act-trader.controller.ts:461
POST /act-trader/get/open/trades act-trader.controller.ts:485
POST /act-trader/get/open/orders act-trader.controller.ts:507
POST /act-trader/get/gethistory act-trader.controller.ts:529
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)
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
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
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
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-ordersexchange) 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.
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.
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.
Services
social-analytics picks them up from there. The rest of auth-service's responsibilities (SSO, broker-auth, OTP, sessions) are covered in User Onboarding.stats-service queries here, not ClickHouse directly.social-analytics; enriches results with calls to connector-hub, users-service, temp-trader.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.
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.exports-queue, exports-v2-queue), two workers, two controllers, same logical pipeline. Pick one; feature-flag cutover; delete the other.@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.stats-service or from the new exporter. The boundary is the reason stats-service has survived as a thin layer.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.DownloadRequests table. All four deps + the table belong in the diagram.POST /stats/import/trigger calls {ANALYTICS_BASE_URL}/api/import/trigger). Choose one as authoritative and document.social-analytics. No business-logic drift into the gateway layer.
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.
exports, exports-scheduler), not the version. Deprecate the legacy path behind a feature flag, then delete.
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.
| ID | Risk today | Resolved by |
|---|---|---|
| R1 | Analytics pipeline bundled with auth-service — a crashing export worker takes down login | Extract exports plugin + scheduler + sync-trade-history to analytics-exporter-service. auth-service keeps only SSO / sessions / verification. |
| R2 | stats-service filter CRUD accepts user_id as a query param with no auth — spoofable | Gateway-level auth guard; user_id derived from JWT. Zero @UseGuards today across all 26 endpoints. |
| R3 | v1 + v2 export surfaces coexist — two queues, two workers, two controllers, same logical pipeline | Pick one, cutover behind a feature flag, delete the other. Halves the ops surface. |
| R4 | Exports plugin has 4 upstream HTTP deps (trader, rewards, connector-hub, trading-fundamentals) — none in the diagram | Diagram update; extracting the service makes the deps an explicit contract of analytics-exporter-service. |
| R5 | DownloadRequests Postgres table — the export-request system of record — is invisible in the diagram | Add to the diagram as the state store for the export feature. It holds status, file_token, error_message, timestamps. |
| R6 | RabbitMQ + Redis infrastructure in stats-service is wired and health-indicated but unused | Remove the modules, or wire them for real (e.g. Redis cache tier for social-analytics calls). Dead infrastructure drifts out of date. |
| R7 | sync-trade-history RMQ consumer (queue info.manual) is unmentioned in the Analytics module | Surface in the diagram or move to the new service alongside the exports plugin. |
| R8 | Ingestion 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
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
Request · query
{ "fileToken": "string (required)", "user_id": "string" }
Response · 200
CSV stream (text/csv)
POST /export/recover auth-service · exports/export.controller.ts:210
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
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
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
Request · body (SocialAnalyticsImportTriggerDto)
{ "force": false, "resyncOnly": false }
Response
(passthrough from social-analytics)
GET /charts/balance stats.controller.ts:118
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
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
Response
{ /* growth, drawdown, equity, last_login_at */ }
GET /charts/my-statistics/all stats.controller.ts:393
Response
{ "growth": ..., "drawdown": ..., "summary": ... }
GET /charts/roi-daily stats.controller.ts:422
Response
{ /* daily ROI series */ }
GET /charts/roi-simulation stats.controller.ts:451
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
Response
{ "closedOrders": [...], "tradePnl": [...], "instruments": [...], "copyList": [...] }
GET /portfolio/summary stats.controller.ts:627
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 ⚠)
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
GET /api/charts/* 17 call sites across stats.service.ts
GET /api/portfolio/summary called by stats-service · stats.service.ts:2105
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).
Responsibilities
- Session lifecycle — creates a copy session when a
startCopyevent arrives on RabbitMQ; closes it onstopCopy. - 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
/zulu). No external vendor touchpoints — Stripe lives in subscriptions, not here.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).
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.
Badge event topics
Known topics per the Confluence catalogue. Implementation status per topic not documented here — ignore.
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
/zulu vhost). No external vendor deps.client-express (via BADGES_SERVICE_URL) and admin-express (per the Confluence admin-linked-services table).
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.
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.
badge.assigned publisherbadge.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.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.@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).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.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.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.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.publish() call in a named producer. No ghost publishers (today: badge.assigned). Contract tests on both sides so drift fails in CI.
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.
POST / PUT / DELETE. GET carries no body. Applies to the six @Get-with-body endpoints in badges.controller.ts.
| ID | Risk today | Resolved by |
|---|---|---|
| R1 | HTML "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. |
| R2 | badges.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. |
| R3 | Six @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). |
| R4 | badges.controller.ts:319 returns 'POR badge logic executed' in the POI success response (copy-paste bug). | Return the correct literal. |
| R5 | HTML 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. |
| R6 | badges.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. |
| R7 | 3 of the 8 consumer bindings (B-POR-V, B-POI-V, B-EA — badges.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. |
| R8 | HTML "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. |
| R9 | HTML "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
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
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
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
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
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
Response · 200
{ data: { message: 'Live Account badge logic executed' } }
GET /trade-history/:userId badges-service · badges.controller.ts:381
Response · 200
{ data: { message: 'Trade History badge logic executed' } }
GET /ea-setup/:userId badges-service · badges.controller.ts:401
Response · 200
{ data: { message: 'EA Setup badge logic executed' } }
GET / badges-service · badges.controller.ts:422
Response · 200
{ data: Badge[] } // shape returned by badgesService.getAllActiveBadges()
Response · 500
{ errorCode: 'BADGE_FETCH_ERROR' }
GET /user/:userId badges-service · badges.controller.ts:438
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
EVENT 🐇 B-MOB-V users-service · handler-remote/index.ts:634 → badges-service
EVENT 🐇 B-TRADE-HIS connector-hub · copy-trading/handler-remote/index.ts:463 → badges-service
EVENT 🐇 B-LIVE-ACC connector-hub · copy-trading/handler-remote/index.ts:469 → badges-service
EVENT 🐇 B-DEMO-ACC connector-hub · copy-trading/handler-remote/index.ts:471 → badges-service
ORPHAN 🐇 B-POR-V consumer wired — no publisher found
ORPHAN 🐇 B-POI-V consumer wired — no publisher found
ORPHAN 🐇 B-EA consumer wired — no publisher found
MISSING 🐇 badge.assigned badges-service → notifications-service · not implemented
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.
Services
stats-service and the social bridge layer.External Integrations
| Service | Provider | Purpose | Protocol |
|---|---|---|---|
| subscription-service | Stripe | Billing & recurring payments | HTTPS / Webhooks |
| communication | Sendgrid | Transactional email | HTTPS |
| auth-service | Apple · Google | Federated SSO | OAuth 2.0 |
| community-connector | Mastodon (self-hosted) | Social feed & follows | REST · ActivityPub |
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-owned 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-express whenever an operator action needs an authorization / access-control check.admin-service.
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. Prepares environment, schema, and source/target connectivity before the main pipeline runs.stats-service) has complete coverage. Referenced by stats-service via the analytics-importer trigger path.Data & Infrastructure
Storage choices are deliberate. Every datastore has a single job and a clear owner.
| Store | Type | Owner | Purpose |
|---|---|---|---|
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
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
| Service | Role | Status | Notes |
|---|---|---|---|
client-express | Public HTTP gateway (client) | up | Consumes MariaDB, Redis, MongoDB (broken), DeepL, FXView pricing. |
admin-express | Admin HTTP gateway | up | Same stores as client-express. |
users-service | Canonical user identity | up | Owns user records. |
auth-service | Sessions / JWT, SSO | up | Apple + Google OAuth. |
client-mysql | Node DAL (not a DB) | up | HTTP facade over MariaDB. |
admin-mysql | Node DAL (not a DB) | up | HTTP facade over MariaDB. |
communication | Transactional email | up | Dispatches via Sendgrid. |
admin-service | Admin domain logic | up | |
flavours-service | Feature flags / white-label config | up |
Account Connection
| Service | Role | Status | Notes |
|---|---|---|---|
connector-hub | Central orchestrator | up | Talks to ActTrader, FTL, FXView; subscribes to multiple external RabbitMQ brokers. |
temp-trader | Demo accounts → S281 | up | |
leaders-service | Leader profiles | up | Integrates with FTL. |
copier-service | Copier subscriptions | up | Integrates with FTL. |
subscriptions | Billing & subscription lifecycle | up | Stripe integration. |
community-connector | Mastodon bridge | up | Single target — Mastodon instance. |
Engagement
| Service | Role | Status | Notes |
|---|---|---|---|
notifications-service | In-app / push fan-out | up | Consumes RabbitMQ events. |
rewards-service | Promotions, referral credits | up | |
badges-service | Gamification badges | up | |
stats-service | Per-user / per-leader stats | up | Uses external analytics source. |
Analytics
| Service | Role | Status | Notes |
|---|---|---|---|
auth-service · also in Onboarding | Exports plugin · ACT-281 → files | up | Plugin exports trades from ACT-281 to disk; to be extracted into its own service in a later phase. |
social-analytics | File ingest → ClickHouse writer | up | Consumes the exported files and populates the ClickHouse analytics store. |
stats-service · also in Engagement | Analytics API for client/admin UIs | up | Reads analytics via social-analytics (not ClickHouse directly). |
Rewards
| Service | Role | Status | Notes |
|---|---|---|---|
rewards-service · also in Engagement | Copy-trading reward engine | up | Session lifecycle on startCopy/stopCopy, BullMQ jobs, month-end settlement, wallet + withdrawal. |
Badges
| Service | Role | Status | Notes |
|---|---|---|---|
badges-service · also in Engagement | Gamification · milestones → history | up | Consumes 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
| Service | Role | Status | Notes |
|---|---|---|---|
signal-junction | Inbound signal entry + fan-out | up | Splits faulty from clean signals. |
signal-synchronizer | Signal repair / synchronization | up | Repair shop for malformed signals. |
signal-processor | Core signal processing | up | Applies copy rules, risk caps, leader-copier fan-out. |
act-signal-processor | ACT-specific signal transformer | up | Normalises to ACT broker order format. |
act-bridge-signal-processor | ACT return-path handler | up | Processes fills back into the pipeline. |
signal-out | Final router to execution venue | up | |
persistence-service | Persistence / ClickHouse writer | up | Off-loads hot-path writes. |
Bridges
ACT Bridge
| Service | Role | Status | Notes |
|---|---|---|---|
act-bridge | Adapter to ACT brokers | up | Speaks ACT native protocol. |
order-update | WebSocket consumer for broker events | up | Fills, partial fills, rejects; feeds back into the trading-flow pipeline. |
node-middleware
| Service | Role | Status | Notes |
|---|---|---|---|
node-middleware | Bridge to terminal platforms (MT4, MT5, cTrader, Discord, Telegram) | up | Sits between the signal pipeline and the actual trading terminals. Handles each platform's native protocol. |
Admin
| Service | Role | Status | Notes |
|---|---|---|---|
admin-express | Admin HTTP gateway | up | Orchestrates 12 backend services for CRM, support, and operations. |
admin-mysql | Node DAL (not a DB) | up | MariaDB-backed DAL. Handles auth and verifies users and admins. |
admin-service | Roles / permissions / teams | up | Access-control domain logic. |
Ops / Data Pipeline
| Service | Role | Status | Notes |
|---|---|---|---|
data-pipeline | Backend data migration | ops | Bash. Data migration pipeline; runs during cutover / catch-up windows. |
data-pipeline-setup | Migration environment setup | ops | Bash. Companion to data-pipeline — prepares schema + connectivity. |
import-history | Historical analytics backfill | ops | NestJs. Referenced by stats-service via the analytics-importer trigger. |
Infrastructure
| Component | Location | Status | Notes |
|---|---|---|---|
| MariaDB | Application Server (host process) | up | Replicates with separate peer machines. |
| PostgreSQL | Application Server | up | Symbol-mapping db used by connector-hub. |
| Redis | Application Server | up | redis-stack image. |
| RabbitMQ (on-host) | Application Server | idle | 0 active connections — awaiting consolidation. |
| RabbitMQ (Social Trader legacy) | external (legacy platform) | primary | vhost /zulu — primary bus today. |
| RabbitMQ (ACT 281 broker) | external (ACT platform) | active | vhost /act — order / fill events. |
| MongoDB | Application Server | ⚠ crash-loop | AVX-less host — mongo:latest incompatible. |
| ClickHouse | external analytics platform | up | Analytical sink for stats-service and persistence-service. |