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 and interrupted-flow recovery¶
When creating the initial challenge, the daemon requests verification from the backend and triggers SMS first:
- 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
Once the same HOME contains an unexpired pending challenge, subsequent starts use the recovery path:
- Foreground TTY: prompts for the current code before sending any authentication request that could replace the challenge
- systemd / Docker / CI (non-tty): without
--verify-code, fails before sending a request, preserves the challenge, and tells you to restart from the same HOME with--verify-code <CODE> - Explicit
--verify-code <CODE>: reuses the cached challenge and does not request another SMS
Important:
- 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, clear the cache, or use
--reset-device: those actions may discard or replace the current challenge. In a foreground TTY, enter the code at the prompt; in non-tty mode, use the same HOME with--verify-code <CODE> - If the challenge has expired, follow the next challenge flow and use the newest SMS code on your phone
- SMS code expires in ~60 seconds, so for Telegram / IM relay scenarios
use
--verify-codeto 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, restart promptly with the code from the same HOME
./futu-opend --setup-only --login-account X --login-pwd Y --platform moomoo \
--verify-code 123456
# Expected: the cached challenge is reused, 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 \
--rest-tls-cert /etc/futu-opend/rest-cert.pem \
--rest-tls-key /etc/futu-opend/rest-key.pem \
--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
The daemon cert and key files must be readable by the futu user; the
health-check or reverse-proxy user must be able to read
/etc/futu-opend/rest-ca.pem.
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 build -t futu-opend-rs:1.7.4 .
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 \
-v /etc/futu-opend/rest-cert.pem:/etc/futu-opend/rest-cert.pem:ro \
-v /etc/futu-opend/rest-key.pem:/etc/futu-opend/rest-key.pem:ro \
-v /etc/futu-opend/rest-ca.pem:/etc/futu-opend/rest-ca.pem:ro \
futu-opend-rs:1.7.4 \
--login-account X --login-pwd Y --platform moomoo \
--rest-port 22222 --grpc-port 33333 \
--rest-keys-file /etc/futu-opend/keys.json \
--rest-tls-cert /etc/futu-opend/rest-cert.pem \
--rest-tls-key /etc/futu-opend/rest-key.pem \
--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
1.7.4.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 a mode-0600 file is recommended for packaged builds:
install -m 600 /dev/null "$HOME/.futu-opend-rs/login-pwd"
# Write one password line with a secure local editor.
./futu-opend --login-account 12345678 \
--login-pwd-file "$HOME/.futu-opend-rs/login-pwd" \
--platform moomoo --rest-port 22222
Packaged macOS binaries have distinct ad-hoc signing identities and do not support unattended cross-binary Keychain sharing. The two writer commands reject before reading a secret.
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 credential store | Linux / Windows; packaged macOS builds only perform bounded compatibility reads | ⭐⭐⭐⭐ |
| 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)
./futu-opend --login-account 12345678 \
--login-pwd-file "$HOME/.futu-opend-rs/login-pwd" ...
# 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 \
--rest-tls-cert /etc/futu-opend/rest-cert.pem \
--rest-tls-key /etc/futu-opend/rest-key.pem \
--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 --cacert /etc/futu-opend/rest-ca.pem \
-H "Authorization: Bearer <key_plaintext>" \
https://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://1.7.4.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