Getting started
Your first terminal
A terminal is one the terminal session that MFX Hub runs for you: it logs in to your broker with your
credential, and everything else — quotes, account state, orders — happens through it. This guide
takes you from an API key to a terminal that is ACTIVE, and then retires it.
Before you start
- An API key: a key id and the private key shown once when the key was issued. MFX Hub keeps only the public half, so a lost private key is rotated, never recovered.
- The ids of the farm, broker and server you were given.
- Your broker login and password. They travel once, over TLS, straight into the platform’s vault; no API ever returns them.
The five steps
- Create a client. Every request is signed with your private key (Ed25519); you never write the headers yourself.
- Request the terminal. The answer is immediate and the state is
REQUESTED: placing it on a host is asynchronous. Anidempotency_keyis required — retry with the same key and you get the same terminal back, never a second one. - Submit the credential. It is stored by reference; the terminal can log in from now on.
- Wait for
ACTIVE. It passes throughPROVISIONING(capacity is preparing it) andLOGGING_IN(the broker is answering).conditionssay why if it stalls — never guess from the state alone. - Retire it when you are done. It drains (
DRAINING) and then it isRETIRED.
The whole flow is one runnable file: samples/first_terminal.py.
What each state means to you
| State | What is happening | What you do |
|---|---|---|
REQUESTED |
Accepted; waiting for capacity with room | Nothing |
PROVISIONING |
A host is preparing the terminal | Submit the credential if you have not |
LOGGING_IN |
The terminal is logging in to your broker | Nothing; read conditions if it takes long |
ACTIVE |
Logged in and healthy | Use it |
OFFLINE |
The host running your terminal is down | Nothing — MFX brings it back or moves it |
DRAINING |
Stopping, at your request or for maintenance | Nothing |
RETIRED |
Gone; its slot is free | Request a new one if you need it |
FAILED |
The broker refused the login, or it did not finish in time | Read conditions, then submit the credential again |
The runnable sample: samples/first_terminal.py
Executed in CI against MFX Hub on every change to the knowledge base.
"""Your first terminal: request it, give it a credential, wait for ACTIVE, retire it.
The runnable half of guides/your-first-terminal.md, against the SDK as it shipped.
It was written against the surface *proposed* for and drifted from the one that landed: a
`SigningKey.from_base64` that does not exist, the required `server` missing from
`submit_credentials`, and `conditions` read as a plain mapping. All three were type errors, and none
could fail the samples job, which only checks that each file parses.
MFX_KEY_ID=... MFX_PRIVATE_KEY=... MFX_FARM_ID=... MFX_BROKER_ID=... MFX_SERVER_ID=... \\
MFX_SERVER=... MFX_LOGIN=... python samples/first_terminal.py
The broker password is typed at a prompt. It is never an argument, an environment variable or a
file, so it reaches no shell history, no process list and no CI log.
"""
from __future__ import annotations
import asyncio
import getpass
import os
import uuid
from mfx import MfxClient, SigningKey
BASE_URL = os.environ.get("MFX_BASE_URL", "https://api.dev.mfxhub.com/v1")
SETTLED = {"ACTIVE", "FAILED", "RETIRED"}
async def main() -> None:
client = MfxClient(
BASE_URL,
os.environ["MFX_KEY_ID"],
SigningKey(os.environ["MFX_PRIVATE_KEY"]),
)
# The same key with the same body returns the same terminal, never a second one.
terminal = await client.terminals.create(
farm_id=os.environ["MFX_FARM_ID"],
broker_id=os.environ["MFX_BROKER_ID"],
server_id=os.environ["MFX_SERVER_ID"],
streaming=True,
idempotency_key=f"first-terminal-{uuid.uuid4()}",
)
print(terminal.id, terminal.state) # REQUESTED
await client.terminals.submit_credentials(
terminal.id,
login=int(os.environ["MFX_LOGIN"]),
password=getpass.getpass("Broker password: "),
server=os.environ["MFX_SERVER"],
idempotency_key=f"first-credential-{uuid.uuid4()}",
)
while terminal.state not in SETTLED:
await asyncio.sleep(5)
terminal = await client.terminals.get(terminal.id)
seen = terminal.conditions.to_dict() if terminal.conditions else {}
print(terminal.state, {name: c.get("status", "?") for name, c in seen.items()})
if terminal.state == "ACTIVE":
await client.terminals.retire(terminal.id, idempotency_key=f"retire-{uuid.uuid4()}")
if __name__ == "__main__":
asyncio.run(main())