Monitoring and stopping a bot

The live bot console, the customer emergency stop and why it disables keys before cancelling, the optional local agent and the eight commands it relays, and what an operator should watch on a venue full of market makers.

6 min readUpdated 3 August 2026console, emergency-stop, agent, monitoring, kill-switch

A bot running on someone else's machine is still fully observable from here, because every order it places is a row in your own tables. The console is a read-back of state the platform was storing anyway — it does not host the strategy, run the bot, or hold its logs. That is what makes it affordable to give to every customer.

The bot console

/hb/console. A full-viewport terminal; the site header and footer are suppressed so the workspace fills the screen.

It shows, live:

  • Resting quotes on both sides, with each one's distance from the touch and how long it has been resting.
  • Fills as they land, deduplicated by order id and cumulative filled amount, so a replay cannot double-count one.
  • Inventory skew and open perpetual positions.
  • A per-market rail, when the account is quoting more than one pair.
  • A health verdict for the bot as a whole, with the remedy attached.

Order book reads are coalesced and shared across everyone watching the same market for a fraction of a tick, so two hundred people watching one pair cost roughly what one costs. The page paints from a WebSocket; a one-shot GET /api/hb/console covers the first render and any browser or proxy that blocks upgrades.

When an account has no activity yet, the console does not replace itself with a signup wall — it shows the terminal it is about to become, with a setup panel in the health column stating which of the four steps is outstanding.

The emergency stop

The red Emergency stop control disables the account's Hummingbot keys and then cancels every resting order.

Keys are disabled first. That makes the bot's next signed request fail with 423 Locked, and only then are the resting orders pulled. Cancelling first accomplishes nothing visible — a market maker re-quotes within a second and the customer watches their own cancel undo itself.

Both halves are on by default and either can be run alone. The reply states plainly what happened, including the part people do not expect: the cancellation is account-wide and includes orders placed by hand, not only the bot's.

Re-enabling is a single action on the API Keys page and keeps the same secret, so nothing in the bot needs reconfiguring. A key disabled this way is stamped as the customer's own disable, which is what stops it laundering an operator block: if an administrator switched the key off, the customer cannot switch it back on and the console says so rather than offering a button that will fail.

The control is disabled when there is nothing to stop — no key trading and no order resting.

The local agent

Everything above is one-way: you see what the bot does because it does it here. The optional agent adds the other direction — starting, stopping and reconfiguring a bot from the exchange's own page.

your machine                                        the exchange
hummingbot --headless
   │  MQTT to 127.0.0.1:1883

bicrypto_agent.py ── signed WebSocket, outbound ──►  /api/hb/agent
   (embedded broker)                                       │

                                                    /hb/console

Nothing connects to the customer. There is no inbound port to open, no SSH key, and the platform holds no credential that could reach their machine. The agent dials out and authenticates with a key the customer minted and can revoke, using the same signed handshake the market-data stream uses.

It needs its own scope

The agent's key must carry hb:control:bot, which is deliberately absent from every trading preset. A key minted to let a bot trade cannot also reconfigure it. The Bot control preset pairs it with hb:read:account and nothing else, so an exposed agent key cannot place an order either.

Existing keys never gain the scope by upgrade — permissions are stored as a literal array at creation — so a customer opts in by minting or editing a key.

Eight commands, and nothing else

Command Effect
status Current strategy status, as the bot's own status prints it
start Start the strategy, optionally naming a script and config
stop Stop it, optionally skipping order cancellation
history Recent trades
config Change strategy parameters on the running bot
import Load a different strategy file
balance limit Set a per-exchange asset limit
balance paper Set a paper-trading balance

The set is enumerated on the server. Nothing that names a file path, a shell command or a Python expression is accepted — names are validated as bare filenames against an allow-list of characters, with no separators, no .. and no drive letters. That ceiling is what keeps a compromise of the platform from becoming code execution on a customer's machine.

Validation happens here rather than at the bot because the bot builds each request inside a try whose failure branch logs and returns without publishing a reply. A misspelled field therefore produces silence, and the operator watches a spinner for the full command timeout and learns nothing.

Setting it up

  1. Mint a key with Bot control at /hb/keys. Without hb:control:bot the agent is refused and says so.

  2. Point Hummingbot at the agent in conf/conf_client.yml:

    mqtt_bridge:
      mqtt_host: 127.0.0.1
      mqtt_port: 1883
      mqtt_commands: true
      mqtt_logger: true
  3. Run the agent with the same interpreter as the bot. It ships inside the connector kit and needs nothing installed — it carries its own MQTT broker.

    conda activate hummingbot
    BICRYPTO_API_SECRET='...' python agent/bicrypto_agent.py \
      --url https://exchange.example --key <API_KEY>

    The secret is passed through the environment, never as a --secret argument: argv is world-readable in ps on most hosts.

  4. Start the bot as usual. The agent discovers it automatically, including its instance id and MQTT namespace, which it reads from the bot's own subscriptions.

Once connected, the console gains a Control tab and a Bot log tab. With no agent the control tab explains how to get one and the log tab is simply empty.

On Windows, Hummingbot's MQTT bridge cannot attach to any broker — ours, mosquitto's or EMQX's. It drives paho through aiomqtt, which registers its socket with loop.add_reader(); Windows' default ProactorEventLoop does not implement that and Hummingbot never sets a selector policy. The TCP connection succeeds and the client then times out during CONNECT, forever.

Run the bot in a Linux guest, under WSL2, in Docker, or on Linux or macOS directly. Everything else works normally on Windows — orders, balances, market data, REST and WebSocket — and the console still shows every order, because those are rows on this side.

If the bot runs in a VM, run the agent inside the guest next to it, so mqtt_host: 127.0.0.1 stays correct and the guest needs no inbound ports. --url is then the exchange's address as seen from inside the guest, which is rarely localhost. Rather than guessing, ask the exchange — /api/hb/ping needs no authentication:

curl -s http://192.168.157.1:4000/api/hb/ping

An answer means that is the address for --url.

What an operator should watch

From /admin/hb and the Command Center:

  • Authentication failures. A sustained rise is usually a drifted clock or a key rotated on one side only, not an attack. Rejections are throttled to one audit row per key per action per minute, so a hammering bot does not flood the table — a small number there can still mean a lot of refusals.
  • Keys issued versus keys used. Long-idle keys are worth expiring.
  • Order-to-fill ratio per account. A bot cancelling and replacing far more than it fills is spending rate-limit budget for nothing and adding load to your database.
  • Concentration. If one bot is most of the volume on a pair, its outage is your liquidity outage.

Cancelling a running bot's orders does not stop it — it re-quotes on its next refresh. To actually stop one account, disable its key first, then cancel. To stop everything, use the global kill switch on the settings screen.