# openroboto The command-line tool for mining on **OpenRoboto**, a Bittensor mainnet subnet (netuid 80) that rewards improvements to vision-language-action models. You fine-tune the current season's base model — LingBot-VLA 2.0 today — publish the checkpoint to Hugging Face, pay a small on-chain evaluation fee, and announce it. The subnet evaluates every submission in simulation with a seed nobody can predict, ranks the results, and pays emissions by rank. Everything you type is this one package. **There is nothing to clone.** ```bash pip install openroboto ``` [Docs index](docs/README.md) · [How the subnet works](docs/SUBNET_OVERVIEW.md) · [This season's base model](docs/MIGRATION.md) · [Evaluation toolkit](https://github.com/openroboto-ai/openroboto-evaluation) Real-robot documentation: [parallel tracks](docs/REAL_TRACKS.md) · [miner preparation](docs/MINER_REAL.md) · [workstation and reference data](docs/WORKSTATION_DATA.md). These pages describe π0.5 and LingBot as parallel physical tracks and publish the confirmed joint-space interface. Publication does not announce a new competition as open. --- ## Requirements - Linux, an NVIDIA GPU (24 GB VRAM minimum) and a recent driver - Python **3.11** - Docker with the NVIDIA Container Toolkit — training runs in a container because openpi needs `numpy<2.0` and bittensor needs `numpy>=2.0`; one interpreter cannot hold both - A registered Bittensor mainnet hotkey with enough TAO for the evaluation fee - A Hugging Face account and a write token ## Your first submission ```bash # 1. Install and scaffold pip install openroboto openroboto init my-miner # a ready-to-use workspace: miner.yaml, # train_strategy.py, README.md, .gitignore cd my-miner $EDITOR miner.yaml # hotkey_ss58, HF token + username # 2. Check everything BEFORE anything costs money openroboto doctor # config, season + fee, HF permissions, balance, Docker, GPU # 3. Build the training image, then train openroboto build openroboto train # 4. Verify the checkpoint format — still free openroboto check # 5. Upload, pay the fee, announce on chain openroboto submit # 6. See what the subnet made of it openroboto status ``` Steps 2 and 4 exist for one reason: **entry fees are not refundable.** The most expensive mistake on this subnet is discovering after paying that the upload was not something the evaluator can load — a bare LoRA adapter, or a checkpoint buried in a subdirectory. `doctor` and `check` are free and catch both. Full walkthrough: [docs/MINER.md](docs/MINER.md). Real-machine setup, systemd, custom strategies: [docs/MINER_DEPLOY.md](docs/MINER_DEPLOY.md). ## Commands | Command | What it does | |---|---| | `openroboto init [DIR] [-s simple\|example] [--validator] [--refresh] [--backend-url URL] [--force]` | Create a working workspace: config, a training strategy to edit, a README with the exact next commands, and a `.gitignore` that keeps your wallet password out of git. ⚠️ **This is the one command that needs the network**: the open competitions come from `GET /api/v1/competitions`, and if that cannot be reached it writes **no file at all** rather than guessing a season. `--refresh` rewrites only the `competition:` section of an existing `miner.yaml`; `--backend-url` says which backend to ask (and the whole workspace is then written to match it); `--force` overwrites existing files | | `openroboto doctor` | Environment check: Python, config, the season's own spec, Docker, GPU, image, HF token, wallet balance against **this season's** fee | | `openroboto build` | Build the training image from the build context shipped inside the package (no clone, no network). Refuses for a competition this client has no image for, rather than filling that competition's image name with the π0.5 one | | `openroboto train [-s script.py]` | Train once; your strategy script is mounted into the container | | `openroboto check [PATH]` | Verify checkpoint layout with the rules the evaluator uses — **no GPU, no network, no second repository** | | `openroboto submit [--config] [--output-dir] [--force]` | Upload → re-check the season against the backend → check the repository layout → pay the entry fee → announce on chain. Resumable from `state/competition_.json`: an upload already done is not repeated and a fee already paid is not paid again | | `openroboto status [--hotkey] [--competition]` | Submission history and scanner rejection reasons (no API key needed) | | `openroboto validator run` | External validator: read published weights, set them on chain | | `openroboto --version` | CLI version and protocol package version | ⚠️ **There is no `openroboto merge`, and none is planned.** Exporting a full checkpoint is part of **training** — it needs the model libraries, which cannot live in the same interpreter as `bittensor` — so it belongs in your training script, inside the training container. The bundled strategies leave that step blank on purpose and say so; `openroboto train` tells you what the run actually produced, and `openroboto check` gives the verdict **before you pay**. ⚠️ **`openroboto submit` checks the layout too, and it is not optional.** Between the upload and the payment it reads the file listing of your HuggingFace repository — the same listing the subnet reads *after* the fee — and stops without paying anything if that listing would not earn a score. There is no flag to skip it, because past that point a rejection is final and the TAO is not refunded. If the listing cannot be fetched at all, it also stops: an answer we could not get is not one this will spend your money on. Run `openroboto check` first anyway — it is free, it runs before the multi-gigabyte upload, and it checks two more rules that need the weight index on your disk. ## Things that will cost you TAO if you skip them The fee is published on the competition you are entering (`params.fee`), and the simulation and real-hardware seasons do not charge the same thing, which is why no figure belongs in your config — or in this file. Never hard-code it: `openroboto submit` confirms it against the backend in the moment before paying, and **refuses to pay at all** if it cannot, rather than reaching for an amount that says how much but not which competition. **`submit` is the only command that pays, and that is on purpose.** The backend only accepts a submission whose payment is within **50 blocks (~10 minutes)** of the chain commitment; this stops a single fee being reused across submissions. Past that window the submission is rejected and the fee is gone. `openroboto submit` runs upload, payment and announcement back-to-back so you stay inside it, and it refuses to publish once the window has closed instead of charging you a second fee for a doomed submission. `upload` / `burn` / `announce` were removed as separate commands in 1.0 for exactly this reason: running one alone is how a fee gets paid for a submission that is never announced. Details: [docs/PAYMENT.md](docs/PAYMENT.md). ## Submission format The evaluator accepts **complete model checkpoints** — an openpi JAX `params/` directory or a PyTorch `model.safetensors`, plus `assets/physical-intelligence/libero/norm_stats.json`. A bare LoRA adapter is rejected by a CPU pre-check before any GPU time is spent. Put them at the **top** of the directory you upload. The evaluator descends two levels looking for the weights and stops; the LingBot exporter's own layout, `checkpoints/global_step_N/hf_ckpt/`, is one level past that, so an unedited upload of a training output is admitted and then never loaded. Exact requirements: [docs/SUBNET_OVERVIEW.md](docs/SUBNET_OVERVIEW.md). ## Verify your evaluation seed Your seed is derived from three public values, two of which did not exist when you submitted — the block hash that carried your commitment, the **competition id** your submission was admitted to, and a drand beacon value. Nobody, including the subnet operator, can pick a seed for a specific miner. You can recompute it: ```bash pip install openroboto-protocol ``` ```python from openroboto_protocol.seed import derive_seed # The middle argument is the competition id, not the payload's `r` and not the # season ordinal the API displays. It is passed by position. seed = derive_seed(block_hash, competition_id, drand_randomness) ``` The backend and this CLI import that exact function — not a copy of it. Formula, drand chain identifier and security assumptions: [docs/SEED_GENERATION.md](docs/SEED_GENERATION.md). ## Docker: what runs where Training **always** runs in a container — openpi needs `numpy<2.0` and bittensor needs `numpy>=2.0`, so one interpreter cannot hold both. You do not have to arrange that: ```bash openroboto build # builds the training image, once openroboto train # starts it for you, data and strategy mounted in ``` The training image definition ships **inside the package**. There is nothing to clone, nothing to keep in sync, and no network needed to build it. You do need Docker on the host. The package ships **two** image definitions, one per base model: `runner/` (openpi, π0.5) and `runner/lingbot/` (LingBot-VLA 2.0). Which one is used follows `competition.base_model_family`, never the adapter name. A competition whose base model has no image here is refused by `build` and `train` — they stop rather than build another base model's image under that competition's name. Train such a season your own way, then come back for `openroboto check` and `openroboto submit`, which both work on a checkpoint this CLI did not produce. ### Running the CLI in a container too (optional, needs a clone) Separately, the `Dockerfile` and `docker-compose.yml` **in this repository** put the CLI itself in a container, for people who would rather not install it into their host Python. That is a repository-level convenience: `openroboto init` does **not** write these files into your workspace, because a compose file that builds the CLI image would need this repository's build context anyway. ```bash git clone https://github.com/openroboto-ai/openroboto-cli && cd openroboto-cli docker compose up train docker compose run --rm train submit --config miner.yaml ``` There is deliberately no `submit` service — a compose service can be restarted, and restarting a command that spends a non-refundable entry fee is not something to leave to a restart policy. ⚠️ This compose file mounts the Docker socket so the containerised CLI can start the training container. That grants host root to the container; run it only on your own machine. Installing with `pip` avoids that entirely, which is why it is the documented path. ## Public trust boundary Public: miner participation, local training, Hugging Face upload, fee payment and chain announcement; chain commitment formats and weight-setting logic; evaluation rules, baseline methodology, LIBERO tooling and seed derivation; and the read-only API contract. (`control.json` is documented for external validators, who fetch `public_key` from it — no miner command reads it.) Not here: held-out task data, the scoring-service deployment, and subnet-owner operational tooling. Seed derivation is public precisely because publishing it gives nothing away — the future block hash and drand value do not exist at submission time. ## Development For contributors. Skip this if you installed from PyPI. ```bash git clone https://github.com/openroboto-ai/openroboto-cli cd openroboto-cli uv sync --locked ``` One repository is enough: `openroboto-protocol` is installed from PyPI at the exact version pinned in `pyproject.toml`. To work against unreleased protocol changes, override it in your environment only — `uv pip install -e ../openroboto-protocol` — and do not commit a `[tool.uv.sources]` path entry. A path source **bypasses the version constraint**, A path source that disagrees with the pin leaves every local and CI run green while resolving a different version than the one published. `--locked` is deliberate: it fails when `uv.lock` does not match `pyproject.toml` instead of silently resolving a different dependency tree. The interpreter is pinned to Python 3.11 by `.python-version` — the version miners run. ```bash bash scripts/lint.sh # mypy + ruff check + ruff format uv run pytest -q # no GPU, no chain, no network uv run coverage run --source=src -m pytest -q uv run coverage report # fails below the threshold in pyproject.toml uvx pre-commit install # optional: the same lint on every commit ``` `.github/workflows/ci.yml` runs these same commands — `scripts/lint.sh` is the single definition of "lint", so local and CI cannot drift. CI also fails on any skipped test: nothing here needs hardware or credentials, so a skip means a test was switched off. Protocol constants (commitment encoding, seed derivation, shared vocabularies) come from `openroboto-protocol` and are never copied in here. `.github/workflows/protocol-guards.yml` enforces that and that the dependency is pinned to an exact version — a floating range would let the miner side and the backend side resolve different code, which is the one thing that package exists to prevent. ## Releasing ```bash git tag v1.0.1 && git push origin v1.0.1 # stable git tag v1.1.0a1 && git push origin v1.1.0a1 # pre-release, for testing ``` The tag triggers the same gates every pull request runs, and the `pypi` environment only accepts `v*` tags, so a stray branch build cannot reach the index. It then waits for a reviewer to approve that environment: uploads cannot be undone and a version number can never be reused, so that approval is the last thing standing between a typo and every miner. `pip install openroboto` resolves to the newest **stable** release. A tester takes a pre-release by pinning it exactly (`pip install openroboto==1.1.0a1`); no miner lands on one by accident. ## Repository map | Path | Purpose | |---|---| | `src/openroboto/` | The package: `commands/`, `chain/`, `huggingface/`, `payment/`, `config/`, `training/`, `templates/` | | `src/openroboto/runner/`, `src/openroboto/runner/lingbot/` | One training-image build context per base model (Dockerfile + the in-container entry script), **both shipped in the wheel** so `openroboto build` works offline | | `docs/` | Miner, validator and reproducibility documentation — index at [docs/README.md](docs/README.md) | | `tests/` | Mirrors `src/`; needs no GPU, chain or network | | `Dockerfile`, `docker-compose.yml` | Optional containerised way to run the CLI | Local configuration, runtime state, logs, caches and model weights are excluded by `.gitignore`. ## License See [LICENSE](LICENSE).