Onboarding Guide: The Journey of a Transaction
Page Info
What: End-to-end walkthrough of three core scenarios — FPS deposit, eDDA deposit, and BST withdrawal — connecting all knowledge points across this manual Audience: Product managers who are new to the deposit/withdrawal business and need to build a holistic understanding quickly Prerequisites: None — this is the starting point Reading time: 8 minutes Owner: Deposit & Withdrawal Product Team
Key takeaway: Three core scenarios connect all knowledge points — FPS deposit follows the Push matching path, eDDA deposit follows the Pull direct debit path, and BST withdrawal follows the auto-approval + bank-securities transfer path. Understanding these three paths gives you a complete picture of the system.
30-Second Overview
User initiates request → Bank statement/instruction exchange → Auto matching/approval → Funds credited/remitted → Reconciliation
The deposit and withdrawal system is moomoo's fund channel layer — the bridge connecting user bank accounts to securities accounts.
| Dimension | Count | Description |
|---|---|---|
| Deposit Methods | 10 | BST, FPS, eDDA (HASE/HSBC), Internet Banking, ATM, Cheque, Bill Payment, Overseas, Airstar |
| Withdrawal Channels | 12 | BST, BOCHK B2E/FPS, HASE/HSBC Internet Banking, SCB/CGB FPS, CHATS, Wire, Cheque |
| Supported Banks | 16 | BOCHK, HSBC, HASE, ICBC, SCB, CGB, CMB, MS, Airstar, EWB, etc. |
| Currencies | 5 | HKD, USD, CNH, JPY, SGD |
| Boundary | System Responsibility | Not System Responsibility |
|---|---|---|
| Entry | Receive user deposit/withdrawal requests | User registration, KYC, account opening |
| Bank Integration | Statement parsing, remittance instructions, eDDA/eDDI | Bank internal processing, SWIFT intermediation |
| Fund Operations | Create SBA procedures to operate ledger | Ledger balance management itself (SBA's responsibility) |
| Risk Control | Invoke blacklist/whitelist, high-risk checks | Risk control rule engine itself |
| Notifications | Trigger deposit/withdrawal result notifications | Push channel management |
Deposit and Withdrawal are two completely independent services — the deposit core is "matching" (money arrived, identify whose it is), while the withdrawal core is "approval" (money is leaving, can it be released).
Choose Your Reading Path by Role
Different roles focus on different areas. First read the three scenarios below to build holistic understanding, then dive deeper based on your role:
Product Manager: Understand business rules + know what to change
Core path: Deposit Methods Overview → Matching & Auto-deposit → Withdrawal Rules Manual → Bank Capability Matrix
Key focus: The If requirements change collapsible block at the bottom of each page — telling you "what code needs to change if this feature changes"
Quick Reference: Deposit Rules Reference · Withdrawal Data Dictionary · Refunds & Reversals · FAQ
Operations Staff: Master procedures + handle exceptions
Core path: Manual Matching Guide → Withdrawal Approval Guide → Reversal/Refund Guide → Scheduled Tasks & Monitoring
Key focus: CRM navigation paths, field mapping tables, decision flow diagrams, SLA timelines
Troubleshooting: Deposit Troubleshooting · Withdrawal Troubleshooting
Developers: Understand system architecture + locate code
Core path: System Architecture & Data Flow → SBA Fund Orchestration → Bank Cards & Authorization
Key focus: Code location annotations in each page (file paths in the If requirements change collapsible blocks)
Below we walk through three real scenarios covering every step of deposit and withdrawal. FPS and eDDA are two fundamentally different deposit models — the former is user-initiated transfer (Push), the latter is system-initiated debit from the user's bank account (Pull). Understanding this distinction is key to understanding the entire deposit system.
Deposit Journey: A HKD 50,000 FPS Deposit
Scenario: User selects "FPS" in the moomoo App to transfer HKD 50,000 to their securities account.
Step 1: User Initiates Application
User taps "Deposit" in the App, selects deposit method fps (method code 3), enters amount 50,000, and selects currency HKD.
The system calls the deposit service's ApplyController.applyAction(), performing the following checks:
| Check | Description |
|---|---|
| User Permissions | Verify WEB/OA signature, confirm user identity |
| Deposit Restrictions | Call AccountLockBusiness::isDepositRestricted() to check if the account is restricted from deposits |
Once checks pass, an Apply record is created, written to the applys_{uid % 100} sharded table:
| Field | Value | Description |
|---|---|---|
status | 0 (PENDING) | User has applied, awaiting matching |
deposit_method | fps | FPS |
amount | 50000 | Applied amount |
currency | HKD | Hong Kong Dollar |
export_bank_id | User-selected bank | Paying bank (user side) |
import_bank_id | System-assigned | Receiving bank (company side) |
Learn More
All 10 deposit methods → Deposit Methods Overview Meaning of every Apply table field → Deposit Rules Reference § Core Fields
Step 2: User Completes FPS Transfer
User switches to their banking app and completes the HKD 50,000 transfer via FPS.
FPS is an instant settlement payment system — funds are immediately debited from the user's bank and credited to the company's sub-account at SCB or CGB.
Step 3: Bank Statement Reaches the System
The SCB/CGB FPS gateway pushes the transaction statement to the system. The statement is written to the flows_{bankType}_{YYYYMM} sharded table with key information:
| Field | Value | Description |
|---|---|---|
| Statement Type | FPS Transfer | FPS transfer identifier |
| Remarks | FPS... | FPS transaction reference number |
| Amount | 50000.00 | Actual received amount |
| Currency | HKD | Hong Kong Dollar |
Statement collection methods vary significantly across banks:
| Bank | Method | Timeliness |
|---|---|---|
| BOCHK | B2E active pull | 3 times daily + 2-hour conversion |
| HSBC | MT910 real-time push | Seconds |
| SCB/CGB | FPS API | Real-time |
| CMB/MS | BST bidirectional link | Event-driven, real-time |
| EWB | CSV + BAI2 files | Manual upload as needed |
Learn More
Complete bank collection frequency table → Architecture & Data Flow Bank FPS capabilities → Bank Capability Matrix
Step 4: Automatic Matching Engine Starts
The matching engine is a set of scheduled tasks, one per bank, running every 3 minutes (e.g., php artisan match:boc).
The engine takes all unprocessed statements with status = PROCESSING and pending applications with status = PENDING, then performs five-dimensional comparison:
| Dimension | Match Field | Match Logic |
|---|---|---|
| 1. Currency | currency | Must match exactly |
| 2. Amount | amount | Match within tolerance (HKD/CNH/JPY: 0~20 difference; USD/SGD: 0~3 difference) |
| 3. Name | en_name / cn_name | Exact or fuzzy match |
| 4. Date | date | Within ±15 days of application date |
| 5. Card Number | bank_card_number | Bank account number match |
Three possible match results:
| Result Code | Meaning | Next Step |
|---|---|---|
| 2 (RESULT_DEPOSIT) | Full match, eligible for auto-deposit | → Proceeds to auto-deposit evaluation |
| 1 (RESULT_NORMAL) | Partial match, requires manual confirmation | → Creates match record, awaits operations review |
| 0 (RESULT_NOT) | No match | → No action |
Our FPS deposit: currency HKD matches, amount 50,000 exact match, name matches, date is today, card number matches → RESULT_DEPOSIT (full match).
Learn More
Complete five-dimensional matching rules and tolerance logic → Automatic Matching Engine What happens when matching fails → Exceptions & Manual Processing
Step 5: Auto-Deposit Evaluation
A successful match does not automatically mean auto-deposit. The system must also check 5 auto-deposit conditions:
| # | Condition | This Transaction |
|---|---|---|
| 1 | Amount does not exceed currency limit | HKD 50,000 < HKD 2,000,000 limit → Pass |
| 2 | Statement has not been manually rejected | reject == 0 → Pass |
| 3 | Within the allowed auto-processing time window | During business hours → Pass |
| 4 | One statement maps to only one user | count(uids) == 1 → Pass |
| 5 | Daily auto-deposit count has not exceeded 10 | First transaction → Pass |
Auto-deposit limits by currency:
| Currency | Max Auto-Deposit Amount |
|---|---|
| HKD | 2,000,000 |
| USD | 300,000 |
| CNH | 2,000,000 |
| JPY | 40,000,000 |
| SGD | 350,000 |
All 5 conditions pass → System determines this as a NORMAL deposit type.
Learn More
6 deposit orchestration modes → SBA Fund Orchestration Currency limits and status codes → Deposit Rules Reference
Step 6: SBA Orchestration Execution
The system sends a deposit orchestration request to SBA (Server Bank Account), creating a Procedure:
Deposit Procedure (NORMAL mode) execution sequence:
1. Check freeze →
2. Credit funds (add to securities account balance) →
3. Release freeze →
4. CompleteSBA is the system's "internal bank" — all operations involving fund balance changes are completed through SBA Procedures, ensuring atomicity and consistency.
Learn More
What is SBA → SBA Concepts & Data Model Trigger conditions for 6 deposit orchestration modes → SBA Fund Orchestration
Step 7: Funds Credited, User Receives Notification
Procedure executes successfully, Apply status updates to 2 (DONE).
System sends a push notification (NOTICE_TYPE_NORMAL), and the user sees "Deposit Successful" in the App.
End-to-end time for this FPS deposit: FPS statement arrives in real-time + matching engine 3-minute cycle + SBA execution in seconds ≈ 3–5 minutes.
Deposit Journey II: A HKD 20,000 eDDA Deposit
Scenario: User has already bound their HSBC bank card and completed eDDA authorization. They select "eDDA Electronic Direct Debit" in the moomoo App to deposit HKD 20,000.
Fundamental Difference Between eDDA and FPS
FPS is user-initiated transfer (Push) — the user operates in their banking app, and after funds arrive, the system needs the matching engine to identify "whose money is this". eDDA is system-initiated debit (Pull) — after the user submits in the moomoo App, the system directly debits from the user's bank account. No matching engine needed, because the system initiated the debit itself and inherently knows who the funds belong to.
Step 1: Prerequisite — eDDA Authorization
Before initiating an eDDA deposit, the user must first complete a one-time eDDA authorization, allowing Futu to directly debit from their bank account.
Authorization status is tracked in the setup_eddis table:
| Status Code | Meaning | Description |
|---|---|---|
| 3 | WAITING | Awaiting authorization initiation |
| 1 | PENDING | Authorization in progress, awaiting bank confirmation |
| 2 | EFFECT | Authorization successful, can deposit |
| 0 | FAIL | Authorization failed |
The system checks authorization progress via SetupEddaCheckAuthorizationJob (polling every 600 seconds), with a maximum wait of approximately 10 days (3000 retries).
Common authorization failure reasons:
| Error Code | Meaning |
|---|---|
MFISAC01 | Incorrect bank account number |
MPP01005~01008 | ID number/phone number/name mismatch |
MPP06001 | Bank account status abnormal |
Learn More
Differences between HSBC eDDA and Hang Seng eDDA → HSBC / Hang Seng Which bank cards support eDDA → eDDA Supported Range (HSBC channel: 15 banks, Hang Seng channel: 12 banks)
Step 2: User Initiates eDDA Deposit
User taps "Deposit" in the App, selects deposit method eddaHSBC (method code 9), and enters amount 20,000 HKD.
After creating the Apply record, the system verifies eDDA authorization status:
Verification logic (verifyEddaBankCard):
Hang Seng eDDA → Must be pre-authorized (status = EFFECT)
HSBC eDDA (same bank) → Must be pre-authorized
HSBC eDDA (cross-bank + online account opening) → Post-authorization allowedOur scenario: HSBC same-bank, authorization is effective → Verification passes.
Apply record:
| Field | Value | Difference from FPS |
|---|---|---|
status | 0 (PENDING) | Same |
deposit_method | eddaHSBC | FPS uses fps |
amount | 20000 | — |
currency | HKD | Same |
Step 3: Async Task Queue Initiates Debit
This is the biggest difference between eDDA and FPS — the user does not need to switch to their banking app.
After the Apply is created, the system automatically triggers a task chain:
ApplyFollowJob (detects method = eddaHSBC)
→ HsbcEddiCreateJob (initiate debit)
→ HsbcEddiResultJob (poll debit result)HsbcEddiCreateJob calls Eddi::applyHsbcEddi() to build the debit request:
| Parameter | Description |
|---|---|
debtor_bank_code | User's bank code (HSBC 3-digit) |
debtor_account_identification | User's bank account number |
debtor_name | User's name |
creditor_bank_code | Futu receiving bank code |
creditor_account_identification | Futu receiving account number |
instructed_amount | 20000 (debit amount) |
instructed_amount_currency | HKD |
The system sends the debit instruction to HSBC via the SBA service. It also writes to the hsbc_eddis table, using a unique index on apply_id to prevent duplicate debits.
Learn More
Deposit method comparison → Deposit Methods Overview
Step 4: Bank Executes Debit
After HSBC receives the eDDI (Electronic Direct Debit Instruction):
- Verifies user account balance
- Debits HKD 20,000 from user's account
- Transfers funds to Futu's account at HSBC
- Returns execution result via eDDA Report
The hsbc_edda_report service receives HSBC's execution report and updates the Procedure status:
| Report Status | Meaning | System Action |
|---|---|---|
| finished | Debit successful | Update Apply real_amount, mark as credited |
| rejected | Debit rejected | Record reject_reason_code, notify user |
Step 5: SBA Credit (Skips Matching Engine!)
Key difference: eDDA deposits completely skip the matching engine.
In the matching service code, eDDA is explicitly excluded:
HsbcMatch matching logic:
if (deposit_method == 'eddaHSBC') → return no match (skip)
HangSengMatch matching logic:
if (deposit_method == 'edda') → return no match (skip)Because the system initiated the debit itself, it inherently knows which user the funds belong to and which application they correspond to — no five-dimensional comparison needed.
After successful debit, SBA directly creates a deposit Procedure to complete the credit.
Step 6: Funds Credited, User Receives Notification
HsbcEddiResultJob polls and detects debit success → Apply status updates to 2 (DONE) → Push notification sent.
End-to-end time from submission to credit: Debit instruction sent in seconds + bank processing typically a few minutes to a few hours (depends on bank processing speed).
FPS vs eDDA: Two Deposit Models Compared
| Dimension | FPS (Push) | eDDA (Pull) |
|---|---|---|
| Fund Direction | User actively transfers in | System debits from user's account |
| User Action | Must switch to banking app | One-click within moomoo App |
| Prerequisites | None | eDDA authorization required first |
| Matching Engine | Five-dimensional matching required | Completely skipped |
| Credit Certainty | Depends on match result | Debit = confirmation |
| Supported Banks | SCB, CGB, BOCHK, etc. | HSBC channel: 15 banks, Hang Seng channel: 12 banks (full list) |
| User Experience | Multiple steps, cross-app | Best experience, one-click deposit |
| Data Tables | applys + flows + matches | applys + hsbc_eddis/hs_eddis + setup_eddis |
| Async Tasks | Matching engine scans periodically (3 min) | Job queue actively progresses |
| Failure Handling | Statement arrived but no match → manual | Bank rejects debit → notify user |
Why is eDDA the core flow?
eDDA achieves the best "one-click deposit within the App" experience — no need to switch banking apps, no waiting for matching. eDDA accounts for approximately 78% of total deposits (HSBC eDDA single channel accounts for 72%, Hang Seng eDDA approximately 6%), making it the dominant method. The HSBC channel supports personal accounts from 15 banks, and the Hang Seng channel supports 12 — it's not limited to HSBC and Hang Seng's own bank cards. Understanding the eDDA authorization-debit-credit chain is key to understanding deposit product design. See eDDA Supported Range.
Withdrawal Journey: A HKD 30,000 BST Withdrawal
Scenario: User initiates a withdrawal in the moomoo App, withdrawing HKD 30,000 to their CMB bank card via the BST channel.
Step 1: User Initiates Withdrawal Request
User taps "Withdraw" in the App, selects their CMB bank card, and enters amount 30,000 HKD.
The system calls the withdrawal service's WithdrawCreator.apply(), performing a series of pre-checks:
| Check | Description |
|---|---|
| Withdrawal Restrictions | Check if user is restricted from withdrawals |
| Blacklist | Call hk-withdraw-blacklist-go to check withdrawal blacklist |
| Risk Control Status | Verify user's risk control status |
| Currency-Market Consistency | HKD belongs to HK market, consistent |
| Available Withdrawal Methods | Check withdrawal methods supported by user's bank card |
| Fees | Calculate withdrawal fee |
| Bank Card Info | Verify bank card is valid |
Once checks pass, the system executes an atomic transaction, completing in one go:
- Create SBA List record
- Create Task record (withdrawal task)
- Create Apply record
- Add Flow operation log
Learn More
Meaning of every withdrawal task field → Withdrawal Data Dictionary § Core Fields All 12 withdrawal channels → Withdrawal Methods Overview
Step 2: Channel Routing — System Selects Withdrawal Method
The system automatically determines the withdrawal channel via calcMethod():
Decision tree:
1. Does the user's bank card support BST?
├─ Yes → CMB (CMBHK) supports BST → method = auto_bs
└─ No → Continue evaluation
2. Is the bank in Hong Kong?
├─ No → method = tele_transfer (cross-border wire)
└─ Yes → method = null (requires manual selection)The CMB bank card supports BST (Bank-Securities Transfer), so the system automatically sets method = auto_bs.
Step 3: Determine Approval Workflow Template
Withdrawal tasks have a three-step approval, but not every transaction goes through all three steps:
| Step | Name | When Required |
|---|---|---|
| Step 1: Audit | High-risk review | Only for high-risk/OM accounts |
| Step 2: Confirm | Confirm instruction | All withdrawals |
| Step 3: Remittance | Remit funds | All withdrawals |
This transaction is not high-risk, so the workflow template is [Confirm, Remittance], skipping Audit.
Step 4: Confirm — Confirm Instruction
Operations staff (or automated system) at the Confirm step:
- Verify bank card — Confirm CMB bank card status is normal
- Confirm withdrawal method —
auto_bs(BST) already auto-set
For the auto_bs channel, the Confirm step does not call SBA startTransfer() (that action is reserved for Remittance).
Action: Click "Submit to Next Step" (NEXT) → Proceeds to Remittance.
Step 5: Remittance — Remit Funds
This is the critical step. For the auto_bs (BST) channel, the system checks auto-withdrawal conditions:
| # | Condition | Description | This Transaction |
|---|---|---|---|
| 1 | Channel is auto_bs | Only BST channel supports auto-withdrawal | Yes → Pass |
| 2 | Not migrated data | Exclude historical data migrated from legacy system | New data → Pass |
| 3 | Auto-withdrawal switch is ON | auto_settings table: corresponding currency status = OPEN | HKD is ON → Pass |
| 4 | Amount within limit | amount ≤ max_amount | 30,000 ≤ configured limit → Pass |
| 5 | Sufficient balance | amount ≤ available_balance | Balance sufficient → Pass |
| 6 | Daily count not exceeded | Same user today < 10 transactions | First transaction → Pass |
All pass → System calls SBA startTransfer() (async), creating a withdrawal Procedure.
Learn More
Complete three-step approval process → Withdrawal Lifecycle Withdrawal channel routing decision tree → Withdrawal Methods Overview § How Channels Are Selected
Step 6: BST Instruction Sent to CMB
SBA withdrawal orchestration sends the withdrawal instruction to the CMB BST service (cmb_stock_trans):
Withdrawal instruction processing chain:
1. Withdrawal Service → SBA Withdrawal Orchestration → CMB BST Service
2. CMB BST Service encrypts request with SM2 national cipher algorithm
3. Sends via Socket to CMB's exit_server
4. CMB processes and returns resultSM2 encryption flow:
- Load Futu private key
- Load CMB public key certificate
- Sign + encrypt request data
- Base64 encode and send
Step 7: Bank Callback & Task Completion
After CMB finishes processing, it calls back the system via the BST bidirectional link:
| Callback Result | Processing |
|---|---|
result = 0 (success) | Task status → 2 (DONE) |
result = -5 (timeout) | Auto-switch to backup exit_server and retry |
result = -6 (bank rejected) | Task marked as failed, requires manual handling |
Our transaction succeeds normally. Task status updates to DONE, and the user receives a "Withdrawal Successful" push notification.
End-to-end time from submission to receipt: BST channel is event-driven, typically seconds to a few minutes.
Deposit vs Withdrawal: Key Differences at a Glance
| Dimension | Deposit | Withdrawal |
|---|---|---|
| Direction | User bank → Securities account | Securities account → User bank |
| Method Count | 10 | 12 |
| Core Mechanism | Automatic matching engine (five-dimensional) | Three-step approval workflow |
| Automation Conditions | 5 auto-deposit conditions | 6 BST auto-withdrawal conditions |
| Daily Limit | 10 per user (auto-deposit) | 10 per user (BST auto) |
| Status Count | 6 (PENDING→DONE) | 6 (PENDING→DONE) |
| SBA Mode | 6 deposit orchestration modes | Unified withdrawal orchestration |
| Risk Control | Deposit blacklist + whitelist | Withdrawal blacklist + high-risk detection |
| Volume Distribution | eDDA ~78%, ICBC ~12%, Others ~10% | — |
Where to Go After Reading
| I want to... | Go to |
|---|---|
| Look up a term | Glossary |
| See the 32-service layered architecture | Architecture & Data Flow |
| See all 10 deposit methods | Deposit Methods Overview |
| See all 12 withdrawal channels | Withdrawal Methods Overview |
| See a specific bank's complete rules | Bank Capability Matrix |
| Understand how risk control blocks transactions | Deposit Troubleshooting, Withdrawal Troubleshooting |
| Learn about refund and reversal mechanisms | Refunds & Reversals |
| Understand SBA orchestration | SBA Concepts & Data Model |