API Key setup¶
Upgrade from "local dev, no auth" to "production-ready scope + limits + audit".
Concept overview¶
One key, one record; the SHA-256 hash lives in keys.json
(plaintext is printed exactly once when generated).
Each key has five kinds of constraints:
- scope — which endpoints it can call.
qot:read/acc:read/trade:simulate/trade:real. - allowed_markets / allowed_symbols / allowed_trd_sides — market / symbol / direction whitelists.
- max_order_value / max_daily_value — per-order and daily caps.
- max_orders_per_minute — rate (sliding window).
- hours_window — time window (local timezone; cross-midnight OK:
22:00-04:00). - allowed_machines — soft machine binding (prevents whole-file copy to another host).
REST / gRPC / core WS / MCP all four entry points share the same keys.json — no need to configure it in four places.
1. Install futucli¶
Same tarball / cargo workspace that produces futu-opend.
2. Generate a read-only key¶
Output:
✅ Generated key "research"
plaintext: fc_8a3f2b9c1e5d7a4b8c2f9e1d3a5b7c9f ← save this; the terminal output is your only copy
stored in: /Users/you/.config/futu/keys.json
Plaintext is printed only once
keys.json only stores the SHA-256 hash. If you lose the plaintext,
revoke and regenerate — there's no way to recover it.
3. Generate a trading key with limits¶
./futucli gen-key \
--id sim-bot \
--scopes qot:read,acc:read,trade:simulate \
--allowed-markets HK,US \
--allowed-trd-sides SELL \
--max-order-value 100000 \
--max-daily-value 500000 \
--max-orders-per-minute 5 \
--hours-window 09:30-16:00 \
--expires 30d
This key can only:
- Place orders in simulate mode (
trade:simulate, nottrade:real). - Sell, not buy (
--allowed-trd-sides SELLwhitelist). - Trade HK / US.
- Up to 100k per order, 500k daily.
- Up to 5 orders per minute.
- Only between 09:30 and 16:00.
- Expires after 30 days.
4. Start the gateway with keys.json¶
./futu-opend \
--login-account 12345678 --login-pwd 'your_pwd' \
--rest-port 22222 --rest-keys-file ~/.config/futu/keys.json \
--grpc-port 33333 --grpc-keys-file ~/.config/futu/keys.json \
--ws-keys-file ~/.config/futu/keys.json \
--audit-log /var/log/futu-audit.jsonl
Startup log changes:
INFO keys_loaded=2 REST keys file loaded (Bearer auth enabled)
INFO keys_loaded=2 gRPC keys file loaded (Bearer auth enabled)
INFO keys_loaded=2 WS keys file loaded (Bearer/?token auth enabled)
INFO audit JSONL logger enabled (target=futu_audit → file)
5. Call each entry point with the key¶
6. List / edit / revoke keys¶
# list all
./futucli list-keys
# revoke one
./futucli revoke-key sim-bot
# edit machine binding in place (no plaintext change)
./futucli bind-key sim-bot --this-machine
./futucli bind-key sim-bot --replace --machines fp_abc123,fp_def456
./futucli bind-key sim-bot --freeze # temporarily disable
./futucli bind-key sim-bot --clear # remove binding
Hot reload
You don't need to restart after editing keys.json — kill -HUP
<pid> does it. All four entry points (REST / gRPC / core WS / MCP)
support SIGHUP hot reload. New keys work within seconds; revoked
keys are rejected immediately.
7. Query the audit log¶
# recent rejects
jq 'select(.outcome=="reject")' /var/log/futu-audit.jsonl | tail -20
# how many orders did sim-bot place today
jq 'select(.key_id=="sim-bot" and .endpoint|test("order"))' /var/log/futu-audit.jsonl | wc -l
# reject reason histogram (DuckDB works too)
jq -r 'select(.outcome=="reject") | .reason' /var/log/futu-audit.jsonl \
| sort | uniq -c | sort -rn
8. Prometheus scrape¶
The /metrics endpoint sits outside the bearer_auth middleware — no
token required:
curl http://localhost:22222/metrics
# futu_auth_events_total{iface="rest",outcome="allow",key_id="research"} 1234
# futu_auth_limit_rejects_total{iface="grpc",key_id="sim-bot",reason="rate"} 7
# futu_ws_filtered_pushes_total{required_scope="trade",key_id="research"} 42
In production, use firewall rules or bind to 127.0.0.1 to restrict
external access to /metrics.