Skip to content

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 plaintext
  • Z = 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.


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 (interactive; will prompt for SMS 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

--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.18 \
  --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):

# Explicitly specify --platform
./futu-opend --login-account X --login-pwd Y --platform moomoo

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

futucli clear-login-pwd --account 12345678

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 IPs unreachable

Futu's overseas IPs are occasionally ISP-blocked. Symptom: every HK / US IP connect times out (10s each), gateway falls into offline mode.

# First confirm whether it's a network issue
for ip in 101.32.217.163 43.135.111.64; do
  nc -vz $ip 9595   # or `timeout 5 bash -c "</dev/tcp/$ip/9595"`
done
  • All FAIL, CN IPs reachable → ISP blocking the overseas range → use a VPN
  • All FAIL, 443 reachable but 9595 not → firewall blocks 9595 → ask IT to open it
  • Partial → v1.4.12's parallel race will auto-pick the reachable IP

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/

Index