Skip to content

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.

./futucli --help

2. Generate a read-only key

./futucli gen-key --id research --scopes qot:read,acc:read

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, not trade:real).
  • Sell, not buy (--allowed-trd-sides SELL whitelist).
  • 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

curl -H "Authorization: Bearer fc_8a3f..." \
     http://localhost:22222/api/accounts
grpcurl -H "authorization: Bearer fc_8a3f..." \
        -d '{"proto_id":1002}' \
        localhost:33333 futu.service.FutuOpenD/Request
# browser / no-header clients: ?token=
websocat 'ws://localhost:44444/?token=fc_8a3f...'

# native clients: Bearer header
websocat -H 'Authorization: Bearer fc_8a3f...' ws://localhost:44444/
# stdio
export FUTU_MCP_API_KEY="fc_8a3f..."
./futu-mcp --keys-file ~/.config/futu/keys.json

# HTTP: client passes Authorization header per call (v1.1+)
./futu-mcp --keys-file ~/.config/futu/keys.json --http-listen 127.0.0.1:38765

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.jsonkill -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.

Next