Cheatsheet¶
Command cheatsheet organized by scenario. Every block is
copy-paste ready — replace X / Y with your own account and
password.
Conventions
X= login account (phone / Futubull ID / moomoo ID / email)Y= password plaintextZ= SMS verification code (sent to phone on first login)
1. Account platform selection¶
Futu has two independent account systems: Futubull
(auth.futunn.com) and moomoo (auth.moomoo.com). Both accept the
three formats (numeric ID / phone / email) as --login-account. The
same phone / email can have independent accounts on both platforms (with
different passwords) — --platform disambiguates.
# Futubull (default, CN / HK) — numeric ID / phone / email (any)
./futu-opend --login-account 12345678 --login-pwd Y
./futu-opend --login-account '+86-13900000000' --login-pwd Y
./futu-opend --login-account 'user@example.com' --login-pwd Y
# moomoo (US / SG / AU / JP / CA) — same three formats + --platform moomoo
./futu-opend --login-account 87654321 --login-pwd Y --platform moomoo
./futu-opend --login-account '+1-4155551234' --login-pwd Y --platform moomoo
./futu-opend --login-account 'user@example.com' --login-pwd Y --platform moomoo
Phone format: region code + dash required
Use +<rc>-<number> strictly, e.g. +86-13900000000 /
+1-4155551234. Anything else fails identification.
2. First deploy (recommended workflow)¶
Strongly recommended: two steps — foreground SMS first, production launch second. This avoids systemd / Docker getting stuck on SMS at startup.
# Step 1: foreground one-off setup (daemon sends SMS to phone immediately,
# then prompts interactively for the code)
./futu-opend --setup-only \
--login-account X --login-pwd Y --platform moomoo
# Step 2: normal launch (auto-uses cached credentials, skips SMS)
./futu-opend --login-account X --login-pwd Y --platform moomoo \
--rest-port 22222 --grpc-port 33333
⚠️ SMS timing (v1.4.84 clarification)¶
During daemon startup, SMS is sent BEFORE the tty check:
- daemon connects to backend → POST
/authority/→code=20+device_verify_sig - daemon calls
req_device_codewhich triggers SMS to your phone ← SMS now sent - daemon waits for code — tty mode: stdin prompt; non-tty: aborts with error
Implications:
- If daemon runs in systemd / Docker / CI (non-tty), it will abort, but
SMS has already been sent to your phone. Restart with --verify-code <CODE>
- ⚠️ Do NOT blindly restart daemon: after receiving the SMS, restart
promptly with --verify-code <CODE>. Within the short cache window, the
daemon reuses the existing verification context instead of requesting another
SMS. If the code expired or you restarted repeatedly, use the newest SMS code
on your phone
- SMS code expires in ~60 seconds, so for Telegram / IM relay scenarios
use --verify-code to skip stdin latency
--verify-code workflow for non-tty environments¶
# 1. First launch without --verify-code → daemon sends SMS then aborts (SMS already received)
./futu-opend --setup-only --login-account X --login-pwd Y --platform moomoo
# daemon log contains "📱 v1.4.84 A1: SMS sent to phone; awaiting verification code via stdin"
# then aborts due to non-tty
# 2. After receiving SMS on phone, restart promptly with the code
./futu-opend --setup-only --login-account X --login-pwd Y --platform moomoo \
--verify-code 123456
# Expected: within the short cache window, no new SMS is triggered and
# verification succeeds
--setup-only completes auth + credential cache and exits immediately —
no servers started. Credentials land in
~/.futu-opend-rs/credentials-<hash>.json and SMS won't be requested
again within ~30 days.
3. Production (unattended)¶
systemd¶
/etc/systemd/system/futu-opend.service:
[Unit]
Description=FutuOpenD Rust Gateway
After=network-online.target
[Service]
Type=simple
User=futu
Environment=FUTU_ACCOUNT=...
Environment=FUTU_PWD=...
ExecStart=/usr/local/bin/futu-opend \
--login-account ${FUTU_ACCOUNT} --login-pwd ${FUTU_PWD} \
--platform moomoo \
--rest-port 22222 --grpc-port 33333 \
--rest-keys-file /etc/futu-opend/keys.json \
--grpc-keys-file /etc/futu-opend/keys.json \
--audit-log /var/log/futu-opend/audit.jsonl
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
Deployment steps:
# 1. Foreground setup to write credentials (as futu user)
sudo -u futu /usr/local/bin/futu-opend --setup-only \
--login-account X --login-pwd Y --platform moomoo
# 2. Enable the systemd service
sudo systemctl enable --now futu-opend
Docker¶
docker run -d --name futu-opend --restart unless-stopped \
-p 11111:11111 -p 22222:22222 -p 33333:33333 \
-v ~/.futu-opend-rs:/root/.futu-opend-rs \
-v /etc/futu-opend/keys.json:/etc/futu-opend/keys.json:ro \
ghcr.io/futuleaf/futu-opend-rs:rs-v1.4.84 \
--login-account X --login-pwd Y --platform moomoo \
--rest-port 22222 --grpc-port 33333 \
--rest-keys-file /etc/futu-opend/keys.json \
--grpc-keys-file /etc/futu-opend/keys.json
Credential volume mount
-v ~/.futu-opend-rs:/root/.futu-opend-rs is critical — after a
container rebuild, device_id and credentials need to persist;
otherwise SMS is triggered on every restart.
4. Multi-instance parallel (Futubull + moomoo side by side)¶
Common scenario: one account on Futubull, one on moomoo; run both
gateways at once. Ports must differ — otherwise futucli's default
127.0.0.1:11111 points at the wrong instance.
# Instance A: Futubull (default ports)
./futu-opend --login-account 12345678 --login-pwd Y1 \
--rest-port 22222 --grpc-port 33333 &
# Instance B: moomoo (ports + 1)
./futu-opend --login-account '+86-13900000000' --login-pwd Y2 --platform moomoo \
--port 11112 --rest-port 22223 --grpc-port 33334 &
v1.4.16+ prints a WARN on startup if a port is already taken.
Client access:
curl http://localhost:22222/api/accounts # Futubull
curl http://localhost:22223/api/accounts # moomoo
5. Debug & development¶
# Verbose logs (protocol frames + HTTP requests/responses)
./futu-opend --login-account X --login-pwd Y --log-level trace
# Auth flow only (POST raw response / salt / tgtgt / error_code)
./futu-opend --login-account X --login-pwd Y --log-level debug 2>&1 \
| grep -E "POST.*raw|salt|tgtgt|error_code"
# JSON structured logs (for Loki / ELK / logging pipelines)
./futu-opend --login-account X --login-pwd Y --json-log
# Separate audit log (auth / trade events only; regular logs unchanged)
./futu-opend --login-account X --login-pwd Y \
--audit-log /var/log/futu-opend/audit.jsonl
6. Recovery¶
device_id locked (ret_type=15 or ret_type=21)¶
# Option A: clear files + fresh setup (recommended, thorough)
./futu-opend --reset-device --setup-only \
--login-account X --login-pwd Y --platform moomoo
# Option B: switch to a specific device_id (e.g. machine migration,
# skips new SMS)
./futu-opend --device-id abcdef0123456789 \
--login-account X --login-pwd Y
# Option C: nuke everything (when all caches are suspect)
rm -rf ~/.futu-opend-rs/
v1.4.17+ auto-rotates device_id on error_code=21 (wrong SMS code) up
to 2 times — usually no manual intervention needed.
Password correct but error_code=2 account/password mismatch¶
Almost always the wrong platform (moomoo account sent to futunn or vice versa):
Connection stalls (Connection timed out)¶
v1.4.10 added a 10s timeout, v1.4.11 picks the IP pool per region, v1.4.12 races 3 IPs in parallel. New versions rarely stall. If it still does:
# See where it's stuck
./futu-opend --login-account X --login-pwd Y --log-level debug
# If you're on an overseas account but the regional IP range is fully
# blocked (ISP filtering), you need a VPN.
Client-side common symptoms¶
| Symptom | Cause | Fix |
|---|---|---|
connect refused :11111 |
Gateway not up / port taken | Check futu-opend console log; v1.4.16+ WARNs on port collision |
account/password mismatch |
Wrong credentials | Fix --login-account / --login-pwd, or add --platform moomoo |
device not authorized |
New device needs SMS | Run --setup-only in a foreground terminal once |
no subscription |
Must subscribe before quoting | POST /api/subscribe before POST /api/quote |
gRPC UNAUTHENTICATED |
No Bearer token or wrong key | See the next section on --grpc-keys-file |
7. Login password secure storage (v1.4.18+)¶
Avoid leaking plaintext passwords via ps aux / ~/.bash_history /
config-file backups. futu-opend resolves the password via a 7-layer
priority chain, and OS keychain is recommended:
# One-time: save to the OS keychain
# (macOS Keychain / Linux Secret Service / Windows Credential Manager)
futucli set-login-pwd --account 12345678
# Interactive password prompt — no echo, no shell history
# Subsequent launches: drop --login-pwd, the keychain is read automatically
./futu-opend --login-account 12345678 --platform moomoo --rest-port 22222
7-layer priority (high → low)¶
| # | Method | Use case | Security |
|---|---|---|---|
| 1 | --login-pwd-file <path> |
Docker secrets / systemd LoadCredential | ⭐⭐⭐⭐⭐ |
| 2 | --login-pwd <plain> |
Legacy scripts (prints a WARN) | ⭐ (argv leak) |
| 3 | --login-pwd-md5 <hex> |
Same, md5 is equivalent to plaintext | ⭐ (argv leak) |
| 4 | FUTU_PWD env var |
CI / cron / bash export | ⭐⭐⭐ (no argv leak) |
| 5 | OS keychain | Long-term local / server (recommended default) | ⭐⭐⭐⭐ |
| 6 | Interactive prompt (when stdin is a tty) | Ad-hoc local dev | ⭐⭐⭐⭐ (no echo, no history) |
| 7 | None of the above | Error out | — |
Deployment scenario guide¶
# A. Long-term local (most recommended)
futucli set-login-pwd --account 12345678 # one-off
./futu-opend --login-account 12345678 ... # every launch auto-reads keychain
# B. Docker (secret mounted as a file)
docker run -v pwd-file:/run/secrets/futu-pwd:ro \
ghcr.io/futuleaf/futu-opend-rs \
--login-account X --login-pwd-file /run/secrets/futu-pwd ...
# C. systemd (LoadCredential from EncryptedDir / TPM)
# /etc/systemd/system/futu-opend.service
[Service]
LoadCredential=login-pwd:/etc/futu/pwd
ExecStart=/usr/local/bin/futu-opend \
--login-account X \
--login-pwd-file ${CREDENTIALS_DIRECTORY}/login-pwd ...
# D. CI / cron (FUTU_PWD env)
FUTU_PWD='...' ./futu-opend --login-account X ...
# E. Ad-hoc local (nothing configured → interactive prompt)
./futu-opend --login-account 12345678 ...
# → "Login password for account 12345678: "
Clear a stored password¶
8. REST / gRPC / core WebSocket auth¶
Strongly recommended in production — otherwise /api/* is open to
anyone who can reach the port.
# 1. Generate an API key (limited scope + expiration)
futucli gen-key \
--keys-file ~/.futu-opend-rs/keys.json \
--scopes quote,trade:read \
--expires 30d
# 2. Start opend with the keys file
./futu-opend --login-account X --login-pwd Y \
--rest-keys-file ~/.futu-opend-rs/keys.json \
--grpc-keys-file ~/.futu-opend-rs/keys.json \
--ws-keys-file ~/.futu-opend-rs/keys.json \
--rest-port 22222 --grpc-port 33333 --websocket-port 44444
# 3. Clients use the Bearer token
curl -H "Authorization: Bearer <key_plaintext>" \
http://localhost:22222/api/qot/get_global_state
See API Key setup for full scope tables and key management.
9. Restricted networks (advanced)¶
HTTPS proxy (debug / capture)¶
# --auth-server explicitly overrides auto-switching (v1.4.15+)
./futu-opend --login-account X --login-pwd Y \
--auth-server http://127.0.0.1:19998
Combined with a self-written HTTP proxy (e.g. mitmproxy) you can capture
the full /authority/salt and POST /authority/ bodies for debugging
account identification issues.
Overseas connection domains unreachable¶
Futu's overseas connection domains are occasionally ISP-blocked. Symptom: every HK / US connection times out (10s each), gateway falls into offline mode.
# First confirm whether it's a network issue
for host in hkconn.futunn.com usconn.moomoo.com; do
nc -vz $host 9595
done
- All FAIL, CN connections reachable → ISP blocking overseas connections → use a VPN
- All FAIL, 443 reachable but 9595 not → firewall blocks 9595 → ask IT to open it
- Partial → the parallel race will auto-pick a reachable connection
10. Credential file locations¶
v1.4.17+ puts all persistent files under ~/.futu-opend-rs/:
~/.futu-opend-rs/
├── device-<hash>.dat # 16-hex device_id (one per account)
├── credentials-<hash>.json # device_sig + tgtgt + rand_key + uid
└── keys.json # (optional) API key file
<hash> = md5(normalized_account)[..16], so +86-13900000000 and
13900000000 share the same file (normalization strips the region code).
When to delete:
| Situation | What to remove |
|---|---|
| device_id locked by the server | device-<hash>.dat + credentials-<hash>.json (or use --reset-device) |
| Password changed | Keep credentials-<hash>.json so SMS retriggers |
| Switching platform (futunn ↔ moomoo) | Nothing — the two platforms have independent caches |
| Total cleanup | rm -rf ~/.futu-opend-rs/ |
11. Per-broker unlock-trade (v1.4.47+)¶
unlock-trade defaults to unlocking all brokers in parallel (the
behavior since v1.4.31). If a "hidden" sub-account is blocking the
unlock, add --security-firm to scope to one broker and isolate the
failure to that one account:
futucli unlock-trade --security-firm hk # HK only (FutuHK / 1)
futucli unlock-trade --security-firm us # US / moomoo only (FutuUS / 2)
futucli unlock-trade # default: all brokers
--security-firm accepts 1-7 numeric values, canonical names (FutuHK
/ FutuUS / FutuSG / FutuAU / FutuCA / FutuMY / FutuJP), or
short aliases (hk / us / sg / au / ca / my / jp). An
invalid value returns a clear error listing the security_firm values
your account actually has.
12. FAQ: Why can't I place orders during off-hours? (v1.4.47+)¶
Symptom: During weekends / holidays / overnight, calls to /api/order
or futu_place_order return generic server errors like "please retry
later" or "request timeout", and changing order_type doesn't help.
Root cause: OpenD (both Rust and C++ versions) is a protocol pass-through layer — it can only submit your order right now to the backend. The Futu NiuNiu / moomoo mobile APP has a separate app-server-side pre-submit queue that caches orders until market open, but this queue is not shared with OpenD.
Things NOT to try:
- HK order_type=AUCTION only applies to same-day pre-open auction
(09:00-09:30) — it doesn't cross days
- US fill_outside_rth=true allows pre-market / after-hours fills
during active sessions — it does NOT mean "can queue on weekends"
- Server response is byte-for-byte identical with or without these
flags during closed hours. Don't waste time.
Correct approaches: 1. Wait for market open and resubmit via OpenD API 2. Use the APP (its independent queue triggers at open)
Supported STOP / MIT / TRAILING_STOP order types are still orders submitted by OpenD to the backend at call time. They are not an off-hours pre-submit queue, so do not treat conditional orders as an APP queue replacement.
Market hours:
| Market | Session |
|---|---|
| HK | Mon-Fri 09:30-16:00 HKT (AUCTION allowed 09:00-09:30) |
| US | Mon-Fri 09:30-16:00 ET (HKT: standard time 22:30-05:00, DST 21:30-04:00) |
| CN A-shares | Mon-Fri 09:30-11:30 + 13:00-15:00 CST |
| SG | Mon-Fri 09:00-17:00 SGT |
| JP | Mon-Fri 09:00-11:30 + 12:30-15:00 JST |
| AU | Mon-Fri 10:00-16:00 AEST/AEDT |
| CA | Mon-Fri 09:30-16:00 EST/EDT |
| MY | Mon-Fri 09:00-12:30 + 14:30-17:00 MYT |
Starting v1.4.47 the daemon auto-detects "generic server error + market
is closed" and appends a 【hint】 section explaining this relationship,
so you don't waste cycles on trial-and-error.
Index¶
- First run: Quick Start
- Go deeper: API Key setup / MCP integration / Production deploy
- Full CLI reference: CLI flags
- Issues: see Contact