01 · DECISION
Deposit-adjusted drawdown with 3-day attribution
Raw AUM drawdown inflates whenever a deposit lands: AUM spikes, then the strategy looks like it immediately lost capital. The performance engine walks a 3-day attribution window to match transaction amounts to AUM jumps before computing peak-to-trough. It was added after late-day deposits landing after the daily snapshot produced phantom -100% drawdowns in production.
02 · DECISION
Reliability tiers: hide numbers that can't be defended
The performance API returns a startBalanceReliable flag when there is no AUM snapshot before the first trade. When the flag is false, the UI hides annualized return and average monthly return entirely. Accounts that connected mid-strategy get a smaller metrics set until a full pre-trade snapshot exists.
03 · DECISION
AUM circuit breaker isolates dead API keys
A snapshot job that retries every failing account wastes cycles on expired keys and slows down active ones. The AUM snapshot worker tracks consecutive failures per account in a Map<string, number>, skips accounts with 3+ consecutive failures, and resets the count hourly. An isRunning guard prevents concurrent execution without a lock library. One expired API key has no effect on the snapshot cadence of live accounts. The job runs every 5 minutes (288 snapshots/day per active account).
04 · DECISION
Position deduplication via 8-decimal-place PnL fingerprint
Not all exchange APIs return stable order IDs across paginated history endpoints. Bybit's position history in particular doesn't guarantee idempotent IDs. The closed-position worker uses a 6-field composite key: exchange + account UID + symbol + side + timestamp + PnL formatted to 8 decimal places via formatToPrecision8(), matching the decimal(18,8) DB column. The fingerprint check in positionExists() runs before every write. Positions are fetched in 7-day chunks with 100ms rate-limit delays to stay within Bybit's pagination limits.
05 · DECISION
Per-community ECS containers via CDK
Each trading community gets its own exchange-bot container, not a shared bot with multi-tenant config. The CDK stack (ExchangeBotStack) takes a communityId as a prop and deploys into a shared ECS cluster imported via CloudFormation cross-stack exports (InfrastructureClusterArn, InfrastructureVPCId). Adding a community is a deploy-exchange-bot.sh COMMUNITY_ID TOKEN call. For operators who want lower AWS costs, a Docker VPS alternative runs the same container at 512MB / 0.25 CPU with the same isolation guarantee per community ID.
06 · DECISION
On-chain fee settlement via Solidity Splitter
Trading fees invoiced to followers go into a Hardhat-tested Solidity Splitter contract on BSC and Arbitrum that distributes to provider, platform, and referral wallets atomically. Each trader's wallet is derived deterministically via BSCWalletService.deriveTraderWallet(traderId, communityId), so wallets don't need manual provisioning per follower. The pipeline runs fully automated: position close triggers invoice, invoice triggers on-chain sweep.
07 · DECISION
Multi-chain gas station with mutex-serialized refills
On-chain operations stall when a wallet runs out of native gas. The wallet-service package runs a gas station per chain (BSC and Arbitrum): it monitors every operational wallet, refills below threshold (0.0002 BNB / 0.0001 ETH) from a dedicated funding wallet, and uses async-mutex to serialize refills so concurrent operations never trigger duplicate transfers. Two modes: withGas() waits for the refill to confirm before the dependent transaction; withGasHybrid() releases the caller as soon as gas lands on-chain. If the funding wallet itself is depleted, callers get a clean error instead of a silent stall.
08 · DECISION
Zero-touch community wallet provisioning
Creating a community is one POST /communities call. The backend derives a deterministic HD branch from keccak256(communityId + WALLET_DERIVATION_SECRET) mod 2^31, persists the address, and queues splitter approval. A background worker then approves the Splitter contract on BSC and Arbitrum for that wallet, gated by an AUM threshold so dormant communities don't burn gas. The splitter_approved and arb_splitter_approved flags in community_bsc_wallets make the worker idempotent. No operator ever touches a key, and the derivation is deterministic, so the address is reproducible from communityId alone if state is ever lost.
09 · DECISION
Dual-channel notifications for security-critical events
The notification router fans events out by user preference (Expo push, Telegram, or both), but security-critical events (API key changes, withdrawals, delinquency) always go to both channels regardless of preference. Push tokens flagged as invalid by Expo get deactivated automatically on the next send failure, so dead devices don't accumulate and concurrent sends don't waste retry budget.