# AGENTS Source: https://docs.joinbase.ai/AGENTS > **First-time setup**: Customize this file for your project. Prompt the user to customize this file for their project. > For Mintlify product knowledge (components, configuration, writing standards), > install the Mintlify skill: `npx skills add https://mintlify.com/docs` # Documentation project instructions ## About this project * This is a documentation site built on [Mintlify](https://mintlify.com) * Pages are MDX files with YAML frontmatter * Configuration lives in `docs.json` * Use the Mintlify MCP server, `https://mcp.mintlify.com`, to edit content and settings via MCP * Use the Mintlify docs MCP server, `https://www.mintlify.com/docs/mcp`, to query information about using Mintlify via MCP ## Terminology ## Style preferences * Use active voice and second person ("you") * Keep sentences concise — one idea per sentence * Use sentence case for headings * Bold for UI elements: Click **Settings** * Code formatting for file names, commands, paths, and code references ## Content boundaries # CONTRIBUTING DOCS Source: https://docs.joinbase.ai/CONTRIBUTING-DOCS # Authoring conventions — BASE docs Working note for everyone (humans + agents) writing pages in this repo. Keep it short; the QA gates in `scripts/` enforce the hard rules automatically. ## Component whitelist Use ONLY these Mintlify components. Anything else is rejected in review: * `Card` * `CardGroup` * `Steps` * `Tip` * `Tabs` * `Accordion` Do not introduce other components (no `Note`, `Warning`, `Frame`, custom MDX components, etc.) without an explicit decision recorded here first. ## Cite-or-omit rule Every factual/technical claim MUST be traceable to a pinned source in `SOURCES.md`. If you cannot cite a canonical source for a statement, **omit it** — do not guess, infer, or paraphrase from memory. * Canonical source of truth: **BASE/`base`** (pinned SHA in `SOURCES.md`). * **Never** source content from `PlatformNetwork/*` or any non-BASE org. * If a value is known but not yet confirmed against source, mark it with the approved `unverified` token (the only placeholder the gates allow) and open an item in `issues.md`. `TODO`/`FIXME`/`lorem` are forbidden. ## Frontmatter template Every `.mdx` page starts with YAML frontmatter: ```yaml theme={"dark"} --- title: "Page title in sentence case" description: "One-sentence summary of what this page covers." --- ``` * `title` and `description` are required on every page. * Use sentence case for `title`. * Optional keys when relevant: `icon`, `sidebarTitle`. ## Terminology (preferred usage) Use these terms exactly; do not invent synonyms. | Term | Usage | | ------------- | -------------------------------------------------------- | | **BASE** | The network's token (uppercase). | | **hotkey** | Operational signing key (one word, lowercase). | | **coldkey** | Custody/ownership key (one word, lowercase). | | **UID** | A neuron's unique slot id within the subnet (uppercase). | | **emissions** | Tokens distributed to participants per epoch. | | **weights** | Validator-set scores assigned to miners. | | **miner** | Participant that produces work/responses. | | **validator** | Participant that scores miners and sets weights. | | **subnet** | The network instance. | | **netuid** | The subnet id. For BASE this is **100**. | Network-specific facts (e.g. `netuid 100`) must match `SOURCES.md`. ## Brand colors * Primary red: `#d91f35` * Success green: `#b2ff22` (ALLOWED — whitelisted by the forbidden-string gate) * Forbidden starter greens: `#16a34a`, `#07c983`, `#15803d` (gate fails on these) * Forbidden paid font: `argent-pixel` / `pjc8nxd` ## Style * Active voice, second person ("you"). * One idea per sentence; keep them concise. * Sentence case for headings. * Bold for UI elements (e.g. Click **Settings**). * Code formatting for file names, commands, paths, and code references. ## Local QA gates Run before every commit / PR (see `scripts/`): ```bash theme={"dark"} bash scripts/qa-all.sh # run all four gates # or individually: bash scripts/qa-forbidden.sh # forbidden strings (greens, PlatformNetwork, paid font, bad org URLs) bash scripts/qa-placeholder.sh # TODO/FIXME/lorem (allows `unverified`) bash scripts/qa-build.sh # mint validate (schema/build) bash scripts/qa-links.sh # mint broken-links ``` Each script exits non-zero on failure and prints a `PASS`/`FAIL` summary. # NAV SLUG MAP Source: https://docs.joinbase.ai/NAV-SLUG-MAP # NAV-SLUG-MAP — BASE docs (IA: validators + miners-per-challenge) Binding contract between `docs.json` navigation and `.mdx` paths. Audience legend: **All** · **Miner** · **Validator** · **Challenge operator** · **Integrator** ## Tab: Overview | Slug | File | Title | Audience | | --------------------------------- | ------------------------------------- | ------------------------------------- | --------------- | | index | index.mdx | Welcome to BASE | All | | concepts/overview | concepts/overview\.mdx | What is BASE? | All | | concepts/how-it-works | concepts/how-it-works.mdx | How the subnet works | All | | concepts/challenges | concepts/challenges.mdx | Challenges explained | All | | concepts/miners-and-validators | concepts/miners-and-validators.mdx | Miners and validators | All | | concepts/weights-and-emissions | concepts/weights-and-emissions.mdx | Weights and emissions | All | | concepts/incentive-mechanism | concepts/incentive-mechanism.mdx | Incentive mechanism | All | | concepts/glossary | concepts/glossary.mdx | Glossary | All | | quickstart | quickstart.mdx | Quickstart (operators and developers) | Validator / All | | concepts/installation | concepts/installation.mdx | Installation | All | | concepts/wallets-and-registration | concepts/wallets-and-registration.mdx | Wallets and registration | All | ## Tab: Validators | Slug | File | Title | Audience | | ------------- | ----------------- | ------------------ | --------- | | validators/\* | validators/\*.mdx | BASE operator pack | Validator | ## Tab: Challenges | Slug | File | Title | Audience | | ----------------------------- | --------------------------------- | ------------------------------- | -------------------------- | | challenges/overview | challenges/overview\.mdx | Challenges on BASE | All / Miner | | challenges/agent-challenge/\* | challenges/agent-challenge/\*.mdx | Agent Challenge full miner pack | Miner | | challenges/prism/\* | challenges/prism/\*.mdx | PRISM full miner pack | Miner + challenge operator | | challenges/bounty-challenge | challenges/bounty-challenge.mdx | Bounty | Miner | | challenges/data-fabrication | challenges/data-fabrication.mdx | Data Fabrication | Miner | | challenges/agent-swe | challenges/agent-swe.mdx | Agent SWE | Miner | | challenges/creating | challenges/creating.mdx | Creating a challenge | Author | | challenges/sdk | challenges/sdk.mdx | Challenge SDK | Author | Hub stubs (not in primary nav, keep for old URLs): * `challenges/agent-challenge.mdx` → pack index * `challenges/prism.mdx` → pack overview ## Tab: Miner hub | Slug | File | Title | Audience | | -------------------------- | ------------------------------ | ------------------------ | -------- | | miners/overview | miners/overview\.mdx | Miner hub | Miner | | miners/quickstart | miners/quickstart.mdx | Miner quickstart | Miner | | miners/wallet-registration | miners/wallet-registration.mdx | Wallet and registration | Miner | | miners/choose-a-challenge | miners/choose-a-challenge.mdx | Choosing a challenge | Miner | | miners/authentication | miners/authentication.mdx | Authentication | Miner | | miners/submitting | miners/submitting.mdx | Submitting (generic) | Miner | | miners/installation | miners/installation.mdx | Installation | Miner | | miners/monitoring | miners/monitoring.mdx | Monitoring | Miner | | miners/troubleshooting | miners/troubleshooting.mdx | Troubleshooting | Miner | | miners/migration-baseagent | miners/migration-baseagent.mdx | Migrating from baseagent | Miner | ## Legacy stubs (not primary tabs) * `agents/*` → `challenges/agent-challenge/*` * `prism/*` → `challenges/prism/*` ## Tab: Reference and Architecture Unchanged path set under `reference/*` and `architecture/*`. ## Product truth 1. BASE docs for operators = validators / subnet infra only 2. Miners compete inside challenges; no undirected Base mining game 3. Agent Challenge production = Phala TDX self-deploy, RA-TLS key release, no Base LLM gateway on scored path # SOURCES Source: https://docs.joinbase.ai/SOURCES # SOURCES — BASE Docs Source Manifest > Generated by Foundation Task T1. This manifest pins every source repository used > to author the BASE Mintlify documentation. All clones are FRESH from > `github.com/BaseIntelligence/*`. SHAs are exact `git rev-parse HEAD` values > captured at clone time. ## SOURCE-PRECEDENCE RULE `base` is the CANONICAL subnet repository. There is NO `baseintelligence/platform` repo. When sources conflict, `base` wins. Every API/CLI/config detail in the docs MUST trace to a source file path:line in one of these cloned repos, or be omitted (cite-or-omit). *** ## Workspace Layout * Docs deliverable repo (the one we write): `/projects/baseintelligence/docs` * Read-only reference clones: `/projects/baseintelligence/sources/` *** ## Repositories ### docs — Documentation deliverable repo (THIS repo) * **GitHub URL:** [https://github.com/BaseIntelligence/docs](https://github.com/BaseIntelligence/docs) * **Clone path:** `/projects/baseintelligence/docs` * **Visibility:** PUBLIC * **Default branch:** main * **Pinned commit SHA:** `c69815515d1df3123e618b1fa3b708e8cadce224` * **GitHub description:** *(none set)* * **Contents:** The Mintlify documentation site for BASE (currently the Mintlify Starter Kit template — `docs.json`, `index.mdx`, `quickstart.mdx`, `logo/`, `favicon.svg`). This is the deliverable repo all docs work targets. ### base — CANONICAL subnet repository * **GitHub URL:** [https://github.com/BaseIntelligence/base](https://github.com/BaseIntelligence/base) * **Clone path:** `/projects/baseintelligence/sources/base` * **Visibility:** PUBLIC * **Default branch:** main * **Pinned commit SHA:** `e33109bfa4f5054928c3b4d429be9cf35d36b166` * **GitHub description:** `[🧠] Platform is a Bittensor subnet enabling decentralized collaborative AI research through multiple challenges, each focused on a specific objective where miners compete and contribute innovative code.` * **Contents:** The canonical Bittensor subnet codebase. Python package `platform_network` (`src/platform_network/`) holds validator, miner auth, weight-setting, CLI app, config/settings, schemas, bittensor integration, alembic migrations, and `deploy/` swarm configs. **Per the precedence rule, this repo wins all conflicts.** ### agent-challenge * **GitHub URL:** [https://github.com/BaseIntelligence/agent-challenge](https://github.com/BaseIntelligence/agent-challenge) * **Clone path:** `/projects/baseintelligence/sources/agent-challenge` * **Visibility:** PUBLIC * **Default branch:** main * **Pinned commit SHA:** `02eeefe24694d7493cf1b7d8c4db17ec25375a43` * **GitHub description:** `[🖥️] agent challenge is a Platform challenge where developers run and monetize terminal-based AI agents, evaluated in isolated environments and rewarded through competitive performance.` * **Contents:** The "agent challenge" — terminal-based AI agents run/monetized and evaluated in isolated environments. ### prism * **GitHub URL:** [https://github.com/BaseIntelligence/prism](https://github.com/BaseIntelligence/prism) * **Clone path:** `/projects/baseintelligence/sources/prism` * **Visibility:** PUBLIC * **Default branch:** main * **Pinned commit SHA:** `6f3e1fb8a5ad5d8ed007334039a85a3168792c61` * **GitHub description:** `[🔬] PRISM is a Platform challenge for decentralized neural architecture search, where miners submit architectures and training recipes to discover scalable AI improvements through competitive evaluation.` * **Contents:** PRISM challenge — decentralized neural architecture search; miners submit architectures + training recipes for competitive evaluation. ### baseagent * **GitHub URL:** [https://github.com/BaseIntelligence/baseagent](https://github.com/BaseIntelligence/baseagent) * **Clone path:** `/projects/baseintelligence/sources/baseagent` * **Visibility:** PUBLIC * **Default branch:** main * **Pinned commit SHA:** `fb98d498f582d8a961b55c3aee535be09648cc51` * **GitHub description:** *(none set)* * **Contents:** BaseAgent — a high-performance autonomous agent for the Term Challenge (per README), using the DeepSeek API. Holds `agent.py`, `src/`, `rules/`, `docs/`, `pyproject.toml`. ### bounty-challenge * **GitHub URL:** [https://github.com/BaseIntelligence/bounty-challenge](https://github.com/BaseIntelligence/bounty-challenge) * **Clone path:** `/projects/baseintelligence/sources/bounty-challenge` * **Visibility:** PUBLIC * **Default branch:** main * **Pinned commit SHA:** `0273be8d47ba32caf48d4a6543a1fd5c34bb685d` * **GitHub description:** `[🤝] bounty-challenge incentivizes bug discovery and software improvement through community-driven reports and suggestions, with rewards based on impact and quality.` * **Contents:** bounty-challenge — incentivizes bug discovery + software improvement via community reports (stub/early-stage repo). ### data-fabrication * **GitHub URL:** [https://github.com/BaseIntelligence/data-fabrication](https://github.com/BaseIntelligence/data-fabrication) * **Clone path:** `/projects/baseintelligence/sources/data-fabrication` * **Visibility:** PUBLIC * **Default branch:** main * **Pinned commit SHA:** `b7097d7657a13c36fb4577ea27f8520d1174c8bf` * **GitHub description:** `[🛢️] data-fabrication is a challenge project from the Platform subnet, where developers are incentivized to create diverse and high-performance datasets. Datasets are evaluated in isolated environments, rewarded based on quality and utility, and continuously improved through encrypted and competitive collaboration.` * **Contents:** data-fabrication challenge — developers incentivized to create diverse high-performance datasets, evaluated in isolated environments (stub/early-stage repo). ### Agent-SWE * **GitHub URL:** [https://github.com/BaseIntelligence/Agent-SWE](https://github.com/BaseIntelligence/Agent-SWE) * **Clone path:** `/projects/baseintelligence/sources/Agent-SWE` * **Visibility:** PUBLIC * **Default branch:** main * **Pinned commit SHA:** `a8df82aa2292988c22ecaa9a114c187bc61bb94e` * **GitHub description:** `[🤖] Agent-SWE is a Platform tool for generating real-code software engineering benchmarks, exporting reproducible task workspaces, and evaluating agent patches with deterministic fail-to-pass tests.` * **Contents:** Agent-SWE — tool for generating real-code SWE benchmarks, exporting reproducible task workspaces, and evaluating agent patches with deterministic fail-to-pass tests. *** ## Clone Failures None. All 8 repositories (1 docs deliverable + 7 references) cloned successfully and are PUBLIC. *** ## Verified Facts (cite-or-omit anchors) * **netuid = 100** is confirmed in source (not just repo descriptions): * `sources/base/src/platform_network/config/settings.py:12` → `netuid: int = 100` (inside `class NetworkSettings`) * Corroborated by: `sources/base/config/validator.example.yaml:3`, `sources/base/config/master.example.yaml:3`, `sources/base/deploy/swarm/submitter/submitter.yaml:29` (with comment at `:21` "replicates the live validator EXACTLY (netuid 100, ...)"), `sources/base/deploy/swarm/master.yaml:7`. * **Internal package name** is `platform_network` and the network `name` defaults to `"platform"` (`sources/base/src/platform_network/config/settings.py:11`). This is an internal/code name — the canonical PUBLIC repo is `base`; there is NO `baseintelligence/platform` repo. *** *Manifest pinned by Task T1. Do not edit SHAs without re-cloning.* # Agent architecture (moved) Source: https://docs.joinbase.ai/agents/architecture Redirect: agent architecture is under Agent Challenge. Moved to [Agent architecture](/challenges/agent-challenge/architecture). # baseagent (moved) Source: https://docs.joinbase.ai/agents/baseagent Redirect: baseagent template docs are under Agent Challenge. Moved to [baseagent template](/challenges/agent-challenge/baseagent). # Best practices (moved) Source: https://docs.joinbase.ai/agents/best-practices Redirect: best practices are under Agent Challenge. Moved to [Best practices](/challenges/agent-challenge/best-practices). # Agent configuration (moved) Source: https://docs.joinbase.ai/agents/configuration Redirect: agent configuration is under Agent Challenge. Moved to [Agent configuration](/challenges/agent-challenge/configuration). # Agent evaluation (moved) Source: https://docs.joinbase.ai/agents/evaluation Redirect: evaluation is under Agent Challenge. Moved to [Evaluation and scoring](/challenges/agent-challenge/evaluation). # Building agents for Agent Challenge Source: https://docs.joinbase.ai/agents/overview This tab moved. Agent Challenge miner and builder docs live under Challenges. Agent developer docs are merged into the **Agent Challenge** miner pack so there is one path: package, submit, Phala TDX self-deploy, score. Overview, quickstart, submit, attestation, evaluation, key release, baseagent, architecture, tools, best practices. ## Direct links * [Quickstart](/challenges/agent-challenge/quickstart) * [baseagent template](/challenges/agent-challenge/baseagent) * [Submit](/challenges/agent-challenge/submit) * [Attestation (Phala TDX)](/challenges/agent-challenge/attestation-phala) * [Evaluation](/challenges/agent-challenge/evaluation) Production scoring is **not** DeepSeek-host-inject-only and **not** validator-deployed miner score jobs. Use the challenge pack for product truth. # Agent quickstart (moved) Source: https://docs.joinbase.ai/agents/quickstart Redirect: Agent Challenge quickstart is under Challenges. Moved to [Agent Challenge quickstart](/challenges/agent-challenge/quickstart). # Submitting an agent (moved) Source: https://docs.joinbase.ai/agents/submitting Redirect: agent submit is under Agent Challenge. Moved to [Submit an agent](/challenges/agent-challenge/submit). Production path continues with Phala self-deploy after upload. # Tools (moved) Source: https://docs.joinbase.ai/agents/tools Redirect: agent tools are under Agent Challenge. Moved to [Tools and capabilities](/challenges/agent-challenge/tools). # Broker Source: https://docs.joinbase.ai/architecture/broker The manager-side broker that dispatches short-lived CPU and GPU evaluation jobs to worker nodes as Swarm replicated-jobs, with a strict GPU contract and untrusted-archive validation. The **broker** runs on the manager and dispatches short-lived evaluation work to worker nodes. Challenge services stay on the manager; the broker is the only path that places jobs onto the worker pool. *Source: `docs/architecture.md:44-46`; `src/platform_network/config/settings.py:100-104`.* The Docker broker service, the Swarm backend, and the GPU placement contract. ## Job dispatch The broker dispatches CPU and GPU jobs as Swarm **replicated-jobs** with `--restart-condition none`, so an evaluation can never auto-restart: * CPU jobs are constrained to `node.labels.platform.workload==cpu`. * GPU jobs (broker `gpu_count > 0`) are constrained to `node.labels.platform.workload==gpu` and request `--generic-resource NVIDIA-GPU=`. *Source: `docs/architecture.md:46-49`; `src/platform_network/config/settings.py:103-104`.* The broker listens on its own port and is addressed internally by service name. *Source: `src/platform_network/config/settings.py:63-65`.* | Setting | Default | Source | | --------------------------------------- | ------------------------------------ | --------------------------------------------- | | `docker.broker_port` | `8082` | `src/platform_network/config/settings.py:64` | | `docker.broker_url` | `http://platform-docker-broker:8082` | `src/platform_network/config/settings.py:65` | | `docker.broker_node_role` | `manager` | `src/platform_network/config/settings.py:72` | | `docker.challenge_placement_constraint` | `node.role==manager` | `src/platform_network/config/settings.py:102` | The broker enforces an image allowlist scoped to a single trusted registry namespace. *Source: `src/platform_network/config/settings.py:67-69`.* ## The GPU contract Broker clients request GPUs with `limits.gpu_count`. `gpu_count=None` or an omitted field means CPU-only and emits no GPU resource. A positive integer requests that many GPUs and is expressed as the Swarm generic resource `--generic-resource NVIDIA-GPU=`. The resource name `NVIDIA-GPU` is case-sensitive and must match the `node-generic-resources` advertisement in the worker `daemon.json`. *Source: `docs/architecture.md:73-75`.* GPU placement is node labels plus generic resources only. A GPU job is constrained to `node.labels.platform.workload==gpu` and acquires a capacity lease before the service is created; the lease is released on cleanup or failure. There is no remote GPU HTTP agent and no device-ID scheduling. *Source: `docs/architecture.md:77`.* ## Internal broker endpoints The broker exposes an internal Docker control surface, used between the master and the broker — not part of the public API: | Endpoint | Source | | ------------------------- | ------------------------------------------------------ | | `POST /v1/docker/run` | `src/platform_network/master/docker_broker.py:669-690` | | `POST /v1/docker/cleanup` | `src/platform_network/master/docker_broker.py:692-701` | | `POST /v1/docker/list` | `src/platform_network/master/docker_broker.py:703-712` | Broker calls require a Bearer token plus the `X-Platform-Challenge-Slug` header. *Source: `src/platform_network/master/docker_broker.py:717-730`.* ## Archive validation Broker archive uploads are treated as untrusted input. The Swarm broker path rejects absolute paths, parent traversal, links, and device members before extraction, and malformed broker images are rejected before any service is created. *Source: `docs/security.md:41-43`.* ## Cleanup Job cleanup is two-layered. The broker `/v1/docker/cleanup` path removes the Swarm service and releases the workload and GPU ledger entries on success and failure. The manager-only supervisor timeout-reaper independently reaps jobs that exceed their timeout, so a crashed or unreachable challenge cannot leak long-running services. *Source: `docs/security.md:45`.* Broker-created challenge jobs must not receive the host Docker socket; the default socket-grant allowlist is empty. The privileged Docker-in-Docker escape hatch is covered on the [Security model](/architecture/security) page. ## Related The worker pool the broker dispatches to. The timeout-reaper that backstops broker cleanup. ## Sources Citations reference the `base` repository pinned at SHA `e33109bfa4f5054928c3b4d429be9cf35d36b166` (see `SOURCES.md`). Paths prefixed with `src/platform_network/` are the internal Python package. # Challenge integration Source: https://docs.joinbase.ai/architecture/challenge-integration How an isolated challenge plugs into the subnet: the weight contract, per-challenge SQLite state on the /data volume, and the trust boundary with the control plane. Each challenge lives in its own repository and owns its submissions, scoring logic, state, and public miner experience. The subnet provides the orchestration layer that makes independent challenges run together as one subnet, and challenges plug in through a small, standard contract. *Source: `README.md:28-30`, `README.md:42-46`.* The challenge integration contract, the SQLite runtime, and isolation rules. ## Per-challenge isolation A challenge runs as a Swarm replicated service with its own OCI image, an internal shared token, public routes behind the proxy, an encrypted overlay network, and a `/data` Swarm volume. Public proxy paths block internal challenge routes. *Source: `docs/architecture.md:57-59`.* | Property | Isolation | | ---------------- | ------------------------------------------------------ | | Code | One repository and image per challenge | | Auth | Per-challenge internal shared token for internal calls | | Network | Encrypted overlay network per challenge | | State | Own SQLite database on its own `/data` volume | | Control-plane DB | Never injected into a challenge | *Source: `docs/architecture.md:57-61`; `docs/security.md:8-11`.* ## The weight contract A challenge implements an async `get_weights()` returning a `dict[str, float]` of hotkey → raw score. The master normalizes returned values, so raw scores are acceptable as long as they are finite and non-negative. ```python theme={"dark"} async def get_weights() -> dict[str, float]: return {"5F...hotkey": 1.0} ``` *Source: `docs/challenge-integration.md:9-14`.* The master collects these over an internal call per epoch; see the [Weights pipeline](/architecture/weights-pipeline). *Source: `src/platform_network/master/challenge_client.py:31-43`.* ## Database contract Generated challenges read their runtime database URL from `CHALLENGE_DATABASE_URL`, which points at a SQLite file on the challenge `/data` Swarm volume: ```text theme={"dark"} sqlite+aiosqlite:////data/challenge.sqlite3 ``` The same URL is used for local runs and the deployed Swarm service. There is no Postgres server per challenge; each challenge mounts its own `/data` volume for the SQLite file and artifacts. *Source: `docs/challenge-integration.md:18-26`; `docs/architecture.md:79-81`.* Challenges must never receive `PLATFORM_DATABASE_URL` or any central control-plane PostgreSQL credential — the shared control-plane PostgreSQL is only for master and validator state. *Source: `docs/challenge-integration.md:28`; `docs/security.md:8`.* ## Async SQLAlchemy Generated challenge templates export a `Base` and a `database` helper. Use normal SQLAlchemy 2.x async ORM patterns with `AsyncSession`, `select()`, model registration, and the FastAPI session dependency. Generated applications call `Base.metadata.create_all` through the async engine during startup after models are imported, creating missing tables for the current model set. *Source: `docs/challenge-integration.md:30-70`.* ## Persistent storage The `/data` Swarm volume is the only persistent store for a challenge — use it for the SQLite database, artifacts, analyzer output, uploaded files, and any local state that should survive restarts. By default the `/data` volume is retained when a challenge service is removed, which protects challenge state from accidental deletion. *Source: `docs/challenge-integration.md:72-76`.* Volume retention and the manual purge path are covered on the [Database and registry](/architecture/database) page. ## Build and publish The generated CI workflow tests the challenge and pushes its Docker image to the container registry on `main` and tags. *Source: `docs/challenge-integration.md:96`.* ## Related How an isolated challenge fits the subnet. Per-challenge SQLite, retention, and the control-plane store. ## Sources Citations reference the `base` repository pinned at SHA `e33109bfa4f5054928c3b4d429be9cf35d36b166` (see `SOURCES.md`). # Database and registry Source: https://docs.joinbase.ai/architecture/database The two-tier data model: a shared control-plane PostgreSQL private to the master and per-challenge SQLite on each challenge's /data Swarm volume. The subnet keeps two strictly separated stores: a **shared control-plane PostgreSQL** for master and validator state, and a **per-challenge SQLite** database on each challenge's `/data` Swarm volume. A challenge never receives the control-plane credential. *Source: `docs/architecture.md:42`, `docs/architecture.md:79-81`; `docs/security.md:7-9`.* The control-plane database settings, the challenge SQLite runtime, and the registry state. ## Control-plane PostgreSQL Master and validator control-plane state uses a single shared PostgreSQL-compatible database URL, addressed by `PLATFORM_DATABASE_URL`. That URL is private to the control-plane process and is never shared with challenge containers. *Source: `docs/architecture.md:42`; `src/platform_network/config/settings.py:55-56`.* In production the control-plane state must use PostgreSQL loaded from a Docker secret or an explicit `PLATFORM_DATABASE_URL`; SQLite is rejected for control-plane state. Dev, test, and local runs may use SQLite for master state. *Source: `docs/security.md:20`.* Relational migrations for the control-plane PostgreSQL live in the `alembic/` directory. *Source: `README.md:165`.* ## Per-challenge SQLite The challenge runtime is SQLite-backed. The master injects `CHALLENGE_DATABASE_URL` pointing at the SQLite file on the challenge `/data` Swarm volume: ```text theme={"dark"} sqlite+aiosqlite:////data/challenge.sqlite3 ``` The subnet does not provision a Postgres server per challenge; each challenge owns its `/data` volume for the SQLite database, artifacts, analyzer output, and local files. A challenge never receives the master or validator control-plane database credential. *Source: `docs/architecture.md:61`, `docs/architecture.md:79-81`; `docs/challenge-integration.md:18-28`.* ## The `/data` volume The `/data` Swarm volume is the only persistent store for a challenge. By default it is **retained** when a challenge service is removed, which protects challenge state and the SQLite database from accidental deletion. *Source: `docs/architecture.md:63`; `docs/challenge-integration.md:72-76`.* ### Manual purge Manual deletion of a retained volume is destructive and should be done only as an explicit operator purge. Inspect the slug volume first, then delete only the matching slug volume: ```bash theme={"dark"} docker volume ls --filter label=platform.challenge.slug= docker volume rm ``` These commands are manual and destructive; the subnet does not provide an automated destructive purge. *Source: `docs/challenge-integration.md:78-88`.* ## Registry state The master tracks active challenges and their emission shares in the registry, with a registry state file on the manager. *Source: `docs/architecture.md:36-38`; `src/platform_network/config/settings.py:32`.* The public registry read is served by the proxy at `GET /v1/registry`; the registry response schema is documented in the [Proxy API](/reference/api-proxy) reference. *Source: `src/platform_network/master/app_admin.py:132-134`.* ## Out of scope This implementation does not include a Postgres server per challenge, automatic backups, restore workflows, high availability, connection pooling, storage resize workflows, challenge migration automation, or automated destructive purge. *Source: `docs/architecture.md:83-85`; `docs/challenge-integration.md:90-92`.* ## Related The per-challenge SQLite contract in depth. Why the control-plane credential never reaches a challenge. ## Sources Citations reference the `base` repository pinned at SHA `e33109bfa4f5054928c3b4d429be9cf35d36b166` (see `SOURCES.md`). Paths prefixed with `src/platform_network/` are the internal Python package. # Master node Source: https://docs.joinbase.ai/architecture/master The Docker Swarm manager node that owns the registry, the single public API, the control plane, and final weight computation for the subnet. The **master** is the Docker Swarm manager node. It owns the central registry, admin operations, the Swarm challenge lifecycle, challenge tokens, emission configuration, and final weight computation. The same node also hosts every long-lived challenge service. *Source: `docs/architecture.md:36-40`; `README.md:32-36`.* Master proxy, registry, weight service, and the Swarm bring-up that runs them. ## The single public API The master hosts one proxy that serves the whole public surface on a single port: the `/v1/registry` and `/v1/weights/latest` reads, the `/health` check, the `/challenges/*` passthrough that bridges miner traffic to challenges, and the token-gated admin routes. *Source: `docs/architecture.md:38`, `docs/architecture.md:40`; `README.md:262-265`.* The proxy app is built by `create_proxy_app`. When a runtime controller is wired in, the admin/registry router (`build_admin_router`) is mounted onto the same app, so there is no separate admin listener or admin port. *Source: `src/platform_network/master/app_proxy.py:263`, `src/platform_network/master/app_proxy.py:534-548`; `src/platform_network/master/app_admin.py:66`.* | Route | Auth | Defined at | | ------------------------------------ | ------------------ | ---------------------------------------------------------- | | `GET /health` | none | `src/platform_network/master/app_proxy.py:320-322` | | `GET /v1/registry` | none (public read) | `src/platform_network/master/app_admin.py:132-134` | | `GET /v1/weights/latest` | none (public read) | `src/platform_network/master/app_admin.py:136-157` | | `GET /challenges/{slug}` passthrough | passthrough | `src/platform_network/master/app_proxy.py:520-525` | | `/admin`, `/v1/admin/challenges/*` | admin token | `src/platform_network/master/app_admin.py:176`, `:202-326` | Full request/response detail for these routes lives in the [API reference](/reference/api). ## Ports The manager publishes its control-plane services on fixed host ports. The default in-container service ports are the proxy on `8080` and the broker on `8082`; the Swarm installer publishes them on the host as `18080` (proxy) and `18082` (broker). *Source: `docs/master/README.md:11-16`; `README.md:262-264`.* `/v1/registry` and `/v1/weights/latest` are served by the proxy on the public port; there is no separate admin service or port. *Source: `README.md:265`.* ## What the master owns * **Registry** — active challenges, their emission shares, and the registry state file at `/var/lib/platform/registry.json`. *Source: `docs/architecture.md:36-38`; `src/platform_network/config/settings.py:32`.* * **Challenge services** — the master runs every active challenge service on the host, pinned with the placement constraint `node.role==manager`. *Source: `docs/architecture.md:40`; `src/platform_network/config/settings.py:102`.* * **Control-plane state** — a single shared PostgreSQL-compatible database addressed by `PLATFORM_DATABASE_URL`, private to the control-plane process and never shared with challenge containers. *Source: `docs/architecture.md:42`; `docs/security.md:7`.* * **Weight computation** — the master collects, normalizes, and serves the final vector; the on-chain submitter performs the Bittensor submission. *Source: `docs/architecture.md:38`; `README.md:127-135`.* ## Epoch cadence The master recomputes weights on an epoch interval. The default `epoch_interval_seconds` is `360`. *Source: `src/platform_network/config/settings.py:28`.* The end-to-end computation is detailed on the [Weights pipeline](/architecture/weights-pipeline) page. ## Supervisor and broker The master also hosts the [broker](/architecture/broker), which dispatches short-lived evaluation jobs to workers, and the [supervisor](/architecture/supervisor), a systemd watchdog that runs the manager-only maintenance loops. *Source: `docs/architecture.md:46`; `docs/master/README.md:39-56`.* The master node is also the validator / hotkey node in the reference topology, but challenge orchestration and on-chain submission are separate concerns — see the [Validator network](/architecture/validator-network) page. ## Related How the master fits into the whole subnet. What the master computes each epoch. ## Sources Citations reference the `base` repository pinned at SHA `e33109bfa4f5054928c3b4d429be9cf35d36b166` (see `SOURCES.md`). # Architecture overview Source: https://docs.joinbase.ai/architecture/overview How the BASE subnet is assembled: a single Docker Swarm master node, manually enrolled worker nodes, and the epoch weight flow that ends on-chain. BASE is a **multi-challenge Bittensor subnet** (netuid `100`). Independent challenges run under one validator network: the subnet routes miner traffic to the right challenge, collects each challenge's raw hotkey weights, normalizes emissions, maps miner hotkeys to Bittensor UIDs, and publishes a final weight vector for validators to submit on-chain. *Source: `README.md:21-36`; `src/platform_network/config/settings.py:12`.* The subnet orchestration layer — master proxy, broker, supervisor, weight aggregation, and the Docker Swarm deployment path. ## The shape of the system The subnet runs as a **single Docker Swarm**. One **master (manager) node** hosts the control plane and every long-lived challenge service; manually enrolled **worker nodes** run only short-lived CPU/GPU evaluation jobs. There is no Kubernetes and no `runtime.backend` selector — the only backend is Swarm. *Source: `README.md:32-36`; `docs/architecture.md:36-40`, `docs/architecture.md:67`.* ```mermaid theme={"dark"} flowchart TB subgraph swarm["Docker Swarm"] subgraph manager["Master (manager) node"] proxy["Subnet master proxy
/v1/registry · /v1/weights/latest · /health"] broker["Broker"] supervisor["Supervisor"] challenges["Challenge services
(one per challenge)"] db[("Control-plane PostgreSQL")] end subgraph workers["Worker nodes"] cpu["CPU eval jobs"] gpu["GPU eval jobs"] end end submitter["On-chain submitter
(validator node)"] chain["Bittensor chain"] proxy --> challenges supervisor -. watchdog .-> broker broker -->|"dispatch short-lived jobs"| cpu broker -->|"dispatch short-lived jobs"| gpu proxy --- db submitter -->|"reads /v1/weights/latest"| proxy submitter -->|"set_weights"| chain ``` ## Components | Component | Where it runs | Responsibility | | ------------------------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | Subnet master proxy | Manager node | Single public API on one port: `/v1/registry`, `/v1/weights/latest`, `/health`, `/challenges/*` passthrough, and token-gated admin | | Broker | Manager node | Dispatches short-lived CPU/GPU evaluation jobs to workers as Swarm replicated-jobs | | Supervisor | Manager node | systemd watchdog running the broker-health, timeout-reaper, image-updater, config-sync, and self-update loops | | Challenge services | Manager node | Long-lived challenge APIs, one isolated service per challenge | | Control-plane PostgreSQL | Manager node | Shared master/validator state, private to the control-plane process | | CPU / GPU jobs | Worker nodes | Short-lived evaluation work, dispatched by the broker | | On-chain submitter | Validator node | Reads the master's final vector and submits it to Bittensor | *Source: `docs/architecture.md:36-55`; `README.md:32-36`, `README.md:262-264`.* The master proxy is created by `create_proxy_app`; when an operator wires a runtime controller into it, the admin/registry router is mounted onto the **same** app, so the registry, weights, health, passthrough, and admin routes are all served on one port. *Source: `src/platform_network/master/app_proxy.py:263`, `src/platform_network/master/app_proxy.py:534-548`; `src/platform_network/master/app_admin.py:66`.* ## The epoch weight flow At each epoch the master collects raw challenge weights, normalizes emissions, maps miner hotkeys to Bittensor UIDs, and publishes a vector that the on-chain submitter posts to the chain: 1. The master tracks active challenges and their emission shares. 2. Each challenge calculates raw hotkey weights from its own scoring rules. 3. The master normalizes challenge outputs, applies configured emissions, and maps hotkeys to UIDs. 4. The on-chain submitter fetches the master's final vector and submits weights to Bittensor at epoch boundaries. *Source: `README.md:127-135`; `src/platform_network/master/service.py:85-139`.* The full, line-by-line pipeline — including how raw scores are cleaned, how emissions are combined, and how UID `0` is excluded — is documented on the [Weights pipeline](/architecture/weights-pipeline) page. ## Trust boundaries Each challenge is isolated: its own repository and OCI image, an internal shared token, public routes behind the proxy, an encrypted overlay network, and its own `/data` Swarm volume for SQLite state. The shared control-plane PostgreSQL is private to the master process and is never injected into a challenge. *Source: `docs/architecture.md:57-61`; `docs/security.md:7-9`.* The [Security model](/architecture/security) page covers authentication, signing, secret handling, and validator trust in full. ## Where to go next The manager node, the single public API, and the control plane it owns. The Docker Swarm topology, overlay networks, and worker enrollment. Epoch collection, normalization, UID mapping, and on-chain submission. Isolation rules, auth, signing, secrets, and validator trust. ## Sources All citations on this page reference the `base` repository (canonical), pinned at SHA `e33109bfa4f5054928c3b4d429be9cf35d36b166` (see `SOURCES.md`), cloned at `/projects/baseintelligence/sources/base`. Paths prefixed with `src/platform_network/` are the internal Python package. # Security model Source: https://docs.joinbase.ai/architecture/security Trust boundaries, isolation rules, authentication and signing, secret handling, and the Swarm runtime boundary that keep challenges and the control plane apart. The subnet's security model is built on **isolation**: challenges are sandboxed from the control plane and from each other, the control-plane database is private to the master, and all privileged surfaces are token-gated, signed, or explicitly fenced. *Source: `docs/security.md:5-14`.* The security model, isolation rules, and the auth/signing primitives. ## Isolation rules * The shared control-plane PostgreSQL is available only to the master or validator control-plane process that owns the deployment; its URL comes from `PLATFORM_DATABASE_URL` or a Docker secret. * Challenges never receive master, validator, or central control-plane PostgreSQL credentials. * Each challenge gets only its own SQLite database on its `/data` Swarm volume. * The submitter never receives master DB credentials. * Internal challenge calls require per-challenge shared tokens. * The public proxy strips sensitive headers and blocks internal challenge paths. *Source: `docs/security.md:7-14`.* ## Authentication and signing There are two distinct auth models on the public API, plus internal tokens used only between the master and its containers. ### Miner request signing Signed miner actions preserve only four headers — `X-Hotkey`, `X-Signature`, `X-Nonce`, and `X-Timestamp` — which are verified against a canonical message before the request is bridged to the challenge. *Source: `docs/security.md:14`; `src/platform_network/security/miner_auth.py:159-162`, `src/platform_network/security/miner_auth.py:96-111`.* ### Admin token Management routes depend on `require_admin`, which accepts the token in an `X-Admin-Token` header or as an `Authorization: Bearer` credential and compares it in constant time. The expected token is loaded from `ADMIN_TOKEN` or the file named by `ADMIN_TOKEN_FILE`. *Source: `src/platform_network/master/app_admin.py:121-130`; `src/platform_network/master/admin/auth.py:10-18`, `src/platform_network/master/admin/auth.py:28-29`.* ### Per-challenge tokens Internal challenge calls — including the master's weight collection — require a per-challenge shared token. Public challenge routes are proxied without exposing the internal control routes. *Source: `docs/security.md:11`; `docs/architecture.md:57-59`.* ### Validator trust Validators run only the submit-only on-chain submitter; it reads the public `/v1/weights/latest` and needs no control-plane database credential. The submitter logs only the **public** hotkey SS58 address, never the private key. *Source: `docs/security.md:10`; `deploy/swarm/submitter/run_submitter.py:30-33`, `deploy/swarm/submitter/run_submitter.py:65-86`.* ## Production policy boundaries The production boundary is stricter than local development: * Dev/test/local runs may use SQLite for master state, but production control-plane state must use PostgreSQL from a Docker secret or an explicit `PLATFORM_DATABASE_URL`; SQLite is rejected for control-plane state. * Challenge runtime state is always SQLite on the challenge `/data` Swarm volume. * Production images must include a tag and a `sha256` digest; untagged references and missing digests are rejected. *Source: `docs/security.md:18-23`.* ## Swarm runtime boundary Challenge services run on the manager node (`node.role==manager`); broker-dispatched evaluation jobs run on worker nodes constrained by `node.labels.platform.workload`. Broker-created challenge jobs must not receive the host Docker socket — the default socket-grant allowlist is empty. *Source: `docs/security.md:27`; `src/platform_network/config/settings.py:74-82`.* Network isolation uses encrypted overlay networks created with MTU 1450. A job requesting `network: none` is attached to a dedicated internal (no external routes) encrypted overlay, because Swarm services cannot attach to the predefined `none` network. *Source: `docs/security.md:29`.* ### Privileged escape hatch A Swarm service cannot run `--privileged` or `--gpus`, so `docker service create` never emits them. A challenge that legitimately needs a privileged Docker-in-Docker job uses the capability-gated escape hatch: the broker runs the job as a direct local `docker run` on a worker node. The escape hatch is the only path that grants privilege, it is gated per challenge, and the container owns its own `/var/lib/docker` volume rather than the host Docker socket. *Source: `docs/security.md:33-35`; `src/platform_network/config/settings.py:70-73`.* ## Secrets Admin tokens, challenge tokens, the control-plane database URL, registry credentials, and wallet material must come from files, environment variables, or Docker secrets. Swarm secrets are mounted inside containers at `/run/secrets/platform/`, and value-bearing secrets reach `docker secret create` via stdin, never as argv. *Source: `docs/security.md:49`.* The control-plane database credential is written only into a Docker secret and must not be printed in logs, service definitions, or evidence. Challenge services receive only per-challenge runtime secrets. *Source: `docs/security.md:31`.* Miner env values submitted to the Agent Challenge are per-submission secrets owned by the challenge, encrypted at rest, and cannot be retrieved after submission. The proxy forwards the request body but must not parse, persist, or log submitted env values. *Source: `docs/security.md:51`.* ## Broker archive and cleanup security Broker archive uploads are treated as untrusted input: the Swarm broker path rejects absolute paths, parent traversal, links, and device members before extraction, and malformed images are rejected before any service is created. *Source: `docs/security.md:41-43`.* Cleanup is two-layered. The broker `/v1/docker/cleanup` path removes the Swarm service and releases the workload and GPU ledger entries on success and failure; the manager-only supervisor timeout-reaper independently reaps jobs that exceed their timeout, so a crashed challenge cannot leak long-running services. *Source: `docs/security.md:45`.* ## Failure behavior If a challenge fails health checks or `get_weights`, its contribution is zero for that epoch; the master does not auto-disable it. For public challenge requests, transport failures at ingress, proxy, or challenge discovery become safe `502` responses. *Source: `docs/security.md:55-57`.* ## Related Job dispatch, the GPU contract, and archive validation. Control-plane PostgreSQL versus per-challenge SQLite. ## Sources Citations reference the `base` repository pinned at SHA `e33109bfa4f5054928c3b4d429be9cf35d36b166` (see `SOURCES.md`). Paths prefixed with `src/platform_network/` are the internal Python package. # Supervisor Source: https://docs.joinbase.ai/architecture/supervisor The manager-only systemd watchdog that runs the broker-health, timeout-reaper, image-updater, config-sync, and self-update loops for the subnet control plane. The **supervisor** is a single watchdog-supervised systemd service on the manager node. It replaces the old Kubernetes CronJobs with one process that runs the control-plane maintenance loops. *Source: `docs/master/README.md:39-41`.* The supervisor systemd unit and the manager-only maintenance loops. ## The unit The unit is `Type=notify` with a 30-second watchdog and runs the master supervisor command against the manager config: ```text theme={"dark"} platform master supervisor --config /etc/platform/master.yaml ``` *Source: `docs/master/README.md:50-54`.* It is installed from `deploy/swarm/platform-supervisor.service` and enabled with systemd. *Source: `docs/master/README.md:41-47`; `README.md:179`.* ## The loops The supervisor loops run on the manager only: | Loop | Responsibility | | ----------------------- | ---------------------------------------------------------- | | broker-health | Watch broker health | | timeout-reaper | Reap jobs that exceed their timeout | | image-updater | Roll control-plane services when a mutable image tag moves | | challenge-image-updater | Roll challenge services when their image tag moves | | config-sync | Keep service config in sync | | self-update | Update the supervisor itself | *Source: `docs/master/README.md:56`; `README.md:179`.* ## Timeout reaper The timeout-reaper is the manager-only backstop for broker cleanup: it independently reaps jobs that exceed their timeout, so a crashed or unreachable challenge cannot leak long-running services even if the broker's own [cleanup path](/architecture/broker) does not run. *Source: `docs/security.md:45`.* ## Image updaters The image updaters resolve the public container-registry tag digest and roll the Swarm services to `tag@sha256:` only when a mutable tag actually moves. No registry pull secret is required for public packages. *Source: `docs/master/README.md:56`; `README.md:184`.* ## Related The manager node the supervisor runs on. The job dispatch the timeout-reaper backstops. ## Sources Citations reference the `base` repository pinned at SHA `e33109bfa4f5054928c3b4d429be9cf35d36b166` (see `SOURCES.md`). # Swarm and miner pool Source: https://docs.joinbase.ai/architecture/swarm The Docker Swarm topology: a single manager node for the control plane and challenge services, plus manually enrolled CPU/GPU worker nodes for short-lived evaluation jobs. First-party deployments of the subnet are **Docker Swarm only**. There is no Helm chart, no Kubernetes manifests, and no `runtime.backend` selector — the only backend is Swarm. *Source: `docs/architecture.md:67`; `README.md:35`.* The Swarm installer, overlay networking, and worker enrollment CLI. ## Topology | Node | Swarm role | Runs | | ---------- | ------------------------------------ | --------------------------------------------------------------------------------- | | Manager | `node.role==manager` | Control plane (proxy / broker / supervisor) and the long-lived challenge services | | CPU worker | `node.labels.platform.workload==cpu` | Short-lived CPU broker jobs | | GPU worker | `node.labels.platform.workload==gpu` | Short-lived GPU broker jobs; advertises `NVIDIA-GPU` as a Swarm generic resource | *Source: `README.md:250-254`; `src/platform_network/config/settings.py:102-104`.* The manager runs every active challenge service on the host, pinned with the placement constraint `node.role==manager`; only short-lived broker jobs are dispatched to workers. *Source: `docs/architecture.md:40`, `docs/architecture.md:44-46`.* ## Bringing up the manager The manager is brought up with `deploy/swarm/install-swarm.sh`, which provisions the master proxy, broker, and challenge services on encrypted overlay networks, plus the systemd supervisor unit. The installer is **dry-run by default**: with no flags it prints every planned mutating command and changes nothing; it mutates only with `--apply`, and every destructive step is behind its own explicit flag. *Source: `docs/architecture.md:67`; `docs/master/README.md:22-37`; `README.md:178`.* The installer initializes the Swarm, creates the encrypted overlay networks (`platform_challenges` and the internal `platform_jobs_internal`, MTU 1450), creates the value-bearing Docker secrets via stdin (never argv), and creates the master proxy, broker, and challenge services. *Source: `docs/master/README.md:37`; `README.md:366`.* ## Networking Swarm networking uses encrypted overlay networks at MTU 1450. The required inter-node ports are: | Port | Protocol | Purpose | | ----------- | --------- | ------------------ | | `2377` | tcp | Cluster management | | `7946` | tcp + udp | Node gossip | | `4789` | udp | VXLAN data plane | | IP proto 50 | ESP | Encrypted overlay | *Source: `README.md:186`.* ## Worker enrollment Workers are enrolled manually with a Swarm join token (no SSH). From the manager, the `platform master worker` CLI group manages them: On the manager, run `platform master worker token --cpu` (or `--gpu`); it prints `docker swarm join --token :2377`. On the worker, install the matching `daemon.json` (the GPU variant advertises `NVIDIA-GPU` and registers the NVIDIA runtime), then run the join command. Back on the manager, run `platform master worker label --workload cpu` (or `gpu`) so the broker schedules jobs onto it. *Source: `docs/architecture.md:51`; `docs/master/README.md:58-80`.* The broker then schedules CPU jobs onto `node.labels.platform.workload==cpu` and GPU jobs onto `node.labels.platform.workload==gpu`. *Source: `docs/master/README.md:80`; `src/platform_network/config/settings.py:103-104`.* ## Resource limits Swarm service resources map CPU and memory to `--limit-cpu` and `--limit-memory`, and PID ceilings to `--limit-pids`. `docker service create` does not support `--memory-swap` or `--security-opt`, so swap limits are not emitted and `no-new-privileges` is enforced daemon-wide via `daemon.json`. *Source: `docs/architecture.md:71`; `README.md:187`.* ## Related How CPU/GPU jobs are dispatched to the worker pool. The manager-only maintenance loops. ## Sources Citations reference the `base` repository pinned at SHA `e33109bfa4f5054928c3b4d429be9cf35d36b166` (see `SOURCES.md`). # Validator network Source: https://docs.joinbase.ai/architecture/validator-network How validators participate: a submit-only on-chain process that fetches the master's final weight vector and posts it to Bittensor. Validators on the subnet run one thing: a **submit-only on-chain submitter**. It fetches the master's final, normalized weight vector over HTTP and submits it to Bittensor with the validator hotkey. It runs no challenge orchestration — all challenge services live on the master node. *Source: `README.md:153-156`; `docs/architecture.md:53-55`.* The submit-only submitter, the weights client, and the validator settings. ## What the submitter does The submitter is a minimal process on the validator node. Every `weights_interval_seconds` it reads `/v1/weights/latest` from the master, submits the fetched vector on-chain, and keeps retrying if the master is unavailable. *Source: `docs/architecture.md:53-55`; `deploy/swarm/submitter/run_submitter.py:1-16`.* It deliberately does **not** run the challenge reconcile loop or open the control-plane database — those belong to the master, not the submitter. *Source: `deploy/swarm/submitter/run_submitter.py:8-17`.* ## The submit loop Each iteration inlines three operations: fetch the latest master vector, validate the payload, then call `set_weights`. ```python theme={"dark"} payload = await runner.weights_client.fetch_latest() failure = runner._validate_weights_payload(payload) if failure is not None: return result = runner.weight_setter.set_weights(payload.uids, payload.weights) ``` *Source: `deploy/swarm/submitter/run_submitter.py:112-144`.* The weights client is pointed at the resolved weights URL, with the configured timeout and retry count. *Source: `deploy/swarm/submitter/run_submitter.py:161-169`; `src/platform_network/config/settings.py:42-52`.* ## Configuration | Setting | Default | Source | | ------------------------------------- | -------------------------------- | -------------------------------------------- | | `netuid` | `100` | `src/platform_network/config/settings.py:12` | | `validator.registry_url` | `https://chain.platform.network` | `src/platform_network/config/settings.py:42` | | `validator.weights_interval_seconds` | `360` | `src/platform_network/config/settings.py:45` | | `validator.weights_timeout_seconds` | `15.0` | `src/platform_network/config/settings.py:46` | | `validator.weights_retries` | `3` | `src/platform_network/config/settings.py:47` | | `validator.weights_freshness_seconds` | `720` | `src/platform_network/config/settings.py:48` | The effective weights URL is `weights_url` when set, otherwise the `registry_url`. *Source: `src/platform_network/config/settings.py:50-52`.* The reference submitter config points `weights_url` at `https://chain.platform.network`, the public master endpoint. *Source: `deploy/swarm/submitter/submitter.yaml:42-43`.* ## Identity and safety At startup the submitter logs its target and the **public** validator hotkey SS58 address so an operator can confirm the submitting identity. It never logs the private key or any secret. A `SIGTERM`/`SIGINT` cancels at the next sleep point, so an in-flight `set_weights` completes fully and the stop never leaves a half-submitted state. *Source: `deploy/swarm/submitter/run_submitter.py:30-33`, `deploy/swarm/submitter/run_submitter.py:65-86`, `deploy/swarm/submitter/run_submitter.py:192-203`.* ## Installation The submitter is a single systemd-managed service on the validator node. It needs only the validator hotkey. *Source: `README.md:398-409`.* The vector the submitter posts is produced entirely by the master — see the [Weights pipeline](/architecture/weights-pipeline) for how it is computed, and the [Weights API](/reference/api-weights) for the response schema. ## Related Where the validator sits in the subnet. The node that computes and serves the vector. ## Sources Citations reference the `base` repository pinned at SHA `e33109bfa4f5054928c3b4d429be9cf35d36b166` (see `SOURCES.md`). # Weights pipeline Source: https://docs.joinbase.ai/architecture/weights-pipeline The epoch weight flow: how the master collects raw challenge weights, normalizes emissions, maps hotkeys to Bittensor UIDs, and publishes the vector the submitter posts on-chain. Every epoch the master turns each challenge's raw hotkey scores into a single, chain-ready weight vector. This page traces that flow end to end, from the per-epoch trigger to the on-chain `set_weights` call. *Source: `README.md:127-135`; `src/platform_network/master/service.py:85-139`.* The weight aggregator, master weight service, and the submit-only on-chain submitter. ## The flow at a glance epoch → master collects raw challenge weights → normalize each challenge → normalize emissions → combine into per-hotkey scores → map miner hotkeys to Bittensor UIDs → normalize the UID vector → publish at `/v1/weights/latest` → the on-chain submitter posts it to the chain. *Source: `src/platform_network/master/service.py:85-139`; `src/platform_network/master/aggregator.py:37-73`.* ```mermaid theme={"dark"} flowchart TB epoch(["Epoch boundary"]) --> collect["1 · Collect raw challenge weights"] collect --> normch["2 · Normalize each challenge's weights"] normch --> normem["3 · Normalize emissions"] normem --> combine["4 · Combine into per-hotkey scores"] combine --> mapuid["5 · Map miner hotkeys to Bittensor UIDs
(drop no-UID; exclude UID 0)"] mapuid --> normuid["6 · Normalize the UID vector"] normuid --> publish["7 · Publish at /v1/weights/latest"] publish --> submit["8 · On-chain submitter posts set_weights"] submit --> chain(["Bittensor chain"]) ``` ## 1. Collect raw challenge weights The master resolves the active challenges and their per-challenge tokens, then asks each challenge for its weights over the internal contract. A challenge that fails to return weights raises and is surfaced as a failure for that epoch. *Source: `src/platform_network/master/service.py:30-39`, `src/platform_network/master/service.py:63-83`.* Each challenge implements the standard contract — an async `get_weights()` returning a `dict[str, float]` of hotkey → raw score. Raw scores are acceptable as long as they are finite and non-negative; the master normalizes them. ```python theme={"dark"} async def get_weights() -> dict[str, float]: return {"5F...hotkey": 1.0} ``` *Source: `docs/challenge-integration.md:9-14`; `src/platform_network/master/challenge_client.py:31-43`.* ## 2. Normalize each challenge's weights For each challenge, raw scores are cleaned and normalized: non-finite and non-positive values are dropped, and the remainder is scaled to sum to `1.0`. A challenge whose cleaned weights sum to zero contributes an empty map. ```python theme={"dark"} def normalize_weights(raw: dict[str, float]) -> dict[str, float]: cleaned = _clean_weights(raw) # finite and > 0 only total = sum(cleaned.values()) if total <= 0: return {} return {hotkey: value / total for hotkey, value in cleaned.items()} ``` *Source: `src/platform_network/master/aggregator.py:9-26`.* ## 3. Normalize emissions The configured emission share of each successful challenge is normalized across all active challenges so the shares sum to `1.0`. If the total is zero, every active challenge gets `0.0`. *Source: `src/platform_network/master/aggregator.py:29-34`.* ## 4. Combine into per-hotkey scores Each challenge's normalized per-hotkey weight is multiplied by that challenge's normalized emission share, and the products are summed per hotkey. Challenges that did not return weights successfully are skipped. ```python theme={"dark"} for result in challenge_results: if not result.ok: continue emission = emissions.get(result.slug, 0.0) for hotkey, weight in normalize_weights(result.weights).items(): hotkey_scores[hotkey] += emission * weight ``` *Source: `src/platform_network/master/aggregator.py:44-49`.* ## 5. Map miner hotkeys to Bittensor UIDs The per-hotkey scores are mapped to on-chain UIDs using the metagraph cache. A hotkey with no UID is dropped, and **UID `0` (the master UID) is excluded** from the miner vector. ```python theme={"dark"} for hotkey, weight in hotkey_scores.items(): uid = hotkey_to_uid.get(hotkey) if uid is None: continue if uid == 0: continue uid_scores[uid] += weight ``` *Source: `src/platform_network/master/aggregator.py:51-60`; `src/platform_network/master/service.py:88`.* The master UID default is `0`. *Source: `src/platform_network/config/settings.py:17`.* ## 6. Normalize the UID vector The UID scores are normalized to sum to `1.0`. If the total is zero (no eligible miners), the vector falls back to `{0: 1.0}` — all weight on the master UID. The result is returned as a `FinalWeights`, sorted by UID. ```python theme={"dark"} total = sum(uid_scores.values()) if total > 0: normalized = {uid: value / total for uid, value in uid_scores.items()} else: normalized = {0: 1.0} ``` *Source: `src/platform_network/master/aggregator.py:62-73`.* ## 7. Publish at `/v1/weights/latest` `compute_latest_response` wraps the final vector in a `MasterWeightsResponse` — the UIDs and weights, the per-hotkey weights, the source-challenge contributions, the configured `netuid` and chain endpoint, and timestamps. `expires_at` is `computed_at` plus the freshness window. *Source: `src/platform_network/master/service.py:92-116`.* The freshness window is `MASTER_WEIGHTS_FRESHNESS_SECONDS = 720` seconds, and `expires_at` is validated to be in the future when the response is built. *Source: `src/platform_network/schemas/weights.py:8`.* The public read is served by the proxy at `GET /v1/weights/latest`; the full response schema is on the [Weights API](/reference/api-weights) page. *Source: `src/platform_network/master/app_admin.py:136-157`.* ## 8. Submit on-chain The on-chain submitter fetches the published vector and submits it to Bittensor with the validator hotkey. In the master's own epoch loop, `run_epoch` calls the weight setter directly when configured to submit. *Source: `src/platform_network/master/service.py:118-139`; `README.md:135`.* The submit path is owned by the [validator network](/architecture/validator-network): the submitter is a separate, submit-only process that reads `/v1/weights/latest` and calls `set_weights`. *Source: `deploy/swarm/submitter/run_submitter.py:89-144`.* ## Failure behavior If a challenge fails its health check or `get_weights`, its contribution is zero for that epoch; the master does not auto-disable it. *Source: `docs/security.md:55`.* ## Related The submit-only process that posts the vector on-chain. The response schema served at `/v1/weights/latest`. ## Sources Citations reference the `base` repository pinned at SHA `e33109bfa4f5054928c3b4d429be9cf35d36b166` (see `SOURCES.md`). Paths prefixed with `src/platform_network/` are the internal Python package. # Agent Challenge Source: https://docs.joinbase.ai/challenges/agent-challenge Hub stub: full Agent Challenge miner documentation lives under Challenges → Agent Challenge. Agent Challenge is a primary challenge on BASE. **Production scoring is miner self-deploy on Phala Intel TDX** (attested review, RA-TLS golden key release, attested Terminal-Bench eval). Full miner pack: Quickstart, submit, attestation, evaluation, key release, troubleshooting, and agent template guides. BASE validators operate subnet infrastructure. They do not replace challenge-specific miner TEE self-deploy. See also: [PRISM](/challenges/prism/overview) · [All challenges](/challenges/overview) · [Miner hub](/miners/overview) # Agent architecture Source: https://docs.joinbase.ai/challenges/agent-challenge/architecture How an Agent Challenge submission is structured and executed: the entrypoint contract, the baseagent loop, and the isolated runtime. Production scored path is miner self-deploy on Phala TDX (attested review then eval). This page describes the agent entrypoint and in-guest tool loop, not a Base-native miner binary. This page describes how a submitted agent is structured and executed - from the fixed entrypoint contract enforced by the challenge to the autonomous loop inside the `baseagent` template. ## The entrypoint contract The Agent Challenge enforces a fixed entrypoint. Every submitted ZIP "must include `agent.py` at the archive root, and that file must define a top-level `class Agent`." Production validators import `agent:Agent`; `submitted_agent.py` is not accepted as the entrypoint. (`agent-challenge/docs/miner/README.md:106-108`) The own-runner driver constructs the agent as `Agent(logs_dir=, model_name=, **extra)` (where `extra` may carry `extra_env`), then calls `setup` once before `run`. (`agent-challenge/scripts/example_agent/agent.py:9-12`) The contract method is (`agent-challenge/scripts/example_agent/agent.py:53`): ```python theme={"dark"} async def run(self, instruction, environment, context): ... ``` `environment.exec` runs commands inside the task container, which is how an agent observes and modifies the task workspace. (`agent-challenge/scripts/example_agent/agent.py:53-63`) ## The baseagent loop The `baseagent` template runs an autonomous loop driven entirely by the model. Its `Agent.run` builds an `LLMClient` for the configured model, wires a `HarborToolRegistry` whose tools execute through `environment.exec`, and runs the agent loop until the task completes. (`baseagent/agent.py:128-175`) The high-level flow is (`baseagent/README.md:135-167`): The loop seeds the conversation and reads the terminal state. Prune or compact messages when the context grows too large, then apply prompt caching. The loop calls `deepseek-v4-pro` for the next action. (`baseagent/README.md:147`) If the model returns tool calls, the loop executes them and feeds results back. With no tool calls, the loop injects a verification prompt; on the second pass it marks the task complete. (`baseagent/README.md:155-165`) ## Context management For long tasks the template estimates token usage and, when messages exceed 85% of usable context, scans backwards, protects the most recent 40,000 tool-output tokens, clears old outputs, and - if still over threshold - applies AI summarization. (`baseagent/README.md:283-317`, `baseagent/src/config/defaults.py:46-53`) ## The isolated runtime Agents are evaluated in isolated environments. The only secrets handed to the agent are the Template configuration may read provider variables from `context.env`. On the **production scored path**, legal LLM paths are measured OpenRouter digests under challenge pins or tools-only agents (no Base LLM gateway). Historical DeepSeek-only host inject documentation is **not** the production TEE narrative. Template examples may still show: `DEEPSEEK_BASE_URL`, `LLM_MODEL`, and `LLM_COST_LIMIT`. (`agent-challenge/src/agent_challenge/evaluation/own_runner/isolation.py:13-15,64-67`) Terminal-Bench production runs through `own_runner`, the only execution backend, which executes the runner image's native Docker environment inside a privileged Docker-in-Docker runner launched as a broker job. (`agent-challenge/README.md:92-95`) Task containers run `--network none` unless a task opts in. (`agent-challenge/README.md:265`) The agent reads DeepSeek configuration from `context.env` (and the process environment) - see [Agent configuration](/challenges/agent-challenge/configuration) for the exact variables. ## Next steps The tool surface available to the agent. The submission lifecycle and isolated evaluation. # Phala TDX attestation Source: https://docs.joinbase.ai/challenges/agent-challenge/attestation-phala Intel TDX measurement, dual review/eval images, domain-separated report_data, and trust-but-audit quote accept. Production miner scoring for Agent Challenge runs on **Phala Cloud CPU Intel TDX** confidential VMs. The validator host may have **no** local TDX. Quotes and measurements are produced in Phala guests and re-verified by the challenge service. Operational CLI steps: [Quickstart](/challenges/agent-challenge/quickstart) and [self-deploy](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/miner/self-deploy.md). Key release detail: [Key release](/challenges/agent-challenge/key-release). Residual risk: table below and [security](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/security.md). ## What is measured A production app-compose plus OS image yield a canonical measurement record: | Field | Role | | --------------- | ----------------------------------------------------------- | | `mrtd` | TD measurement root | | `rtmr0` | Runtime measurement register 0 | | `rtmr1` | Runtime measurement register 1 | | `rtmr2` | Runtime measurement register 2 | | `compose_hash` | Hash of the measured compose definition (dstack-compatible) | | `os_image_hash` | Product formula identity from MRTD + RTMR1 + RTMR2 | Product `os_image_hash`: ```text theme={"dark"} os_image_hash = sha256(MRTD || RTMR1 || RTMR2) # lowercase hex of decoded registers ``` `rtmr3` is treated as runtime and is excluded from the static allowlist pin used in score binding helpers; event-log replay still recovers compose identity and `key_provider` from RTMR3 events. Miners can reproduce the six-field record with the self-deploy measurements command (challenge repo). Validators publish **dual** allowlists (review image vs canonical/eval image). A single-field mismatch is `NOT-IN-LIST`. An empty allowlist fails closed. ## Separate report\_data domains TDX quotes carry a 64-byte `report_data` field. Agent Challenge binds a **domain-separated** canonical JSON preimage so stages cannot authorize each other. | Domain tag | Stage | Typical bound content | | ------------------------------------ | ------------- | ------------------------------------------------------------------------------------------------- | | `base-agent-challenge-review-v1` | Review report | Session/report digests, measurement subset, bound `issued_at` / `received_at` (≤24h on re-verify) | | `base-agent-challenge-keyrelease-v1` | Key release | `eval_run_id`, key-release nonce, RA-TLS SPKI digest, schema\_version | | `base-agent-challenge-v1` | Score result | measurement, agent\_hash, task\_ids, scores\_digest, eval\_run\_id, score\_nonce | Mixing domains is a verification failure. Guest wall clock alone never authorizes review freshness. Unattested DB phase labels (`review_allowed`) are cache only and never alone admit eval CVM or a production score. ## Review CVM vs eval CVM | | Review CVM | Eval CVM | | ------------------ | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | | Guest | CPU Intel TDX on Phala | Separate CPU Intel TDX on Phala | | Image | Measured review compose (harness + agent ZIP under `.rules`) | Measured canonical/eval compose with baked live-task-cache | | Secrets | OpenRouter + review session via `encrypted_env` only (**no** Base LLM gateway) | Eval capability / plan via `encrypted_env`; agent OpenRouter only when measured digests allow | | Work | Attested LLM review (no golden tasks) | k-trial benchmark with DooD isolation; golden decrypt only after key release | | Quote domains | review-v1 | keyrelease-v1 then score-v1 | | Golden AES-256 key | Never | Only after successful RA-TLS release; grant is durable for the run | ```mermaid theme={"dark"} sequenceDiagram participant M as Miner participant C as Challenge participant R as Review CVM participant E as Eval CVM participant K as Key release M->>C: signed submit and review prepare M->>R: deploy measured review image R->>C: review report plus review-domain quote C-->>M: allow or reject or escalate M->>C: eval prepare with fresh allow materials M->>E: deploy measured canonical image E->>E: GetTlsKey client cert material E->>K: raw RA-TLS mTLS keyrelease domain K-->>E: AES-256 golden key or deny E->>C: POST result score domain C-->>M: receipt phase and reason_code ``` ## Quote verification (trust-but-audit) Operators and auditors can re-check quotes with `dcap-qvl verify` or Phala hosted verify. Challenge acceptance is a **conjunction** of quote integrity, measurement allowlist, event log, domain binding, nonces, review freshness (bound times ≤24h on re-verify), and (for scores) durable key-grant state. This is cryptographically-anchored trust-but-audit. It is not a claim that TEE hardware is free of class attacks. ## Residual risks | Risk class | Design response | | ------------------------------------------ | ----------------------------------------------------------------------- | | Hardware / TEE.fail-class research | Allowlist plus quote verify; auditors re-check quotes | | Ops pin drift | Dual allowlists; single-field mismatch denies key and score | | Provider outages (Phala, OpenRouter, DCAP) | Fail closed; no silent accepted scores | | Ungrounded miner image | Unknown `compose_hash` fails closed | | Domain mixup | Separate review / keyrelease / score domains | | Replay | Single-use nonces; bound eval\_run\_id; fresh review re-verify | | Key theft | RA-TLS mTLS + SPKI binding; HTTP `POST /release` disabled in production | | Residual CVM cost | Money cap and mandatory teardown | ## Operational bounds * CPU TDX only (`tdx.small` / `tdx.medium`). GPUs refused * Hard projected spend cap (default **\$20** for review+eval lifetime) before create * Mandatory teardown: `phala cvms list` should show `total: 0` after cleanup * No key-grant means no accepted score (even if a guest claimed a number) ## Related * [Key release](/challenges/agent-challenge/key-release) * [Evaluation](/challenges/agent-challenge/evaluation) * [Attestation TEE (repo)](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/miner/attestation-tee.md) # baseagent template Source: https://docs.joinbase.ai/challenges/agent-challenge/baseagent Required Harbor-compatible base implementation for Agent Challenge ZIP submissions. `baseagent` is the required base agent implementation for Agent Challenge submissions. Build from [`BaseIntelligence/baseagent`](https://github.com/BaseIntelligence/baseagent), then package and self-deploy per [Quickstart](/challenges/agent-challenge/quickstart). ## What it is A high-performance autonomous agent that: * Exposes Harbor-compatible `agent:Agent` from root `agent.py` * Runs tool loops inside the task workspace through the challenge environment * Is designed for constrained, reproducible Terminal-Bench style evaluation ## ZIP entrypoint Every submitted ZIP must include `agent.py` at the archive root with top-level `class Agent`. Harbor runners import `agent:Agent`. ```python theme={"dark"} class Agent: async def setup(self, environment): self.environment = environment async def run(self, instruction, environment, context): ... ``` Typical project layout (from the template): ```text theme={"dark"} baseagent/ ├── agent.py # Harbor ZIP entrypoint ├── src/ │ ├── core/ # loop, compaction │ ├── llm/ # model client │ ├── config/ # defaults │ ├── tools/ # tool implementations │ └── prompts/ ├── rules/ └── docs/ ``` ## LLM policy on the scored path Production Agent Challenge scoring is **attestation-only**: * Do **not** embed Base LLM gateway material (`BASE_LLM_GATEWAY_URL`, `BASE_GATEWAY_TOKEN`, `/llm/v1`) * Do **not** hardcode non-measured provider secrets for silent host inject * Review LLM work uses measured OpenRouter under challenge `.rules` (keys via Phala `encrypted_env`) * Eval may allow measured OpenRouter digests inside the guest; tools-only agents are valid Local development against a provider of your choice for drafting code is separate from production allowlisted measured paths. Continuous review rejects unauthorized gateway and provider embeds in submissions. ## Related * [Submit](/challenges/agent-challenge/submit) * [Evaluation](/challenges/agent-challenge/evaluation) * [baseagent repository](https://github.com/BaseIntelligence/baseagent) # Best practices Source: https://docs.joinbase.ai/challenges/agent-challenge/best-practices Build Agent Challenge submissions that clear review, self-deploy cleanly, and earn scores. These practices follow the production Phala TDX path. ## Build a reliable agent A strong agent is reliable, reproducible, and safe inside constrained environments. Within a task it should: * read instructions and repository context * inspect files and failing behavior * modify source safely * run relevant checks when available * avoid destructive or unrelated changes * finish within timeouts * keep secrets out of outputs ## Honor the scored LLM policy * No Base LLM gateway embeds * No non-measured provider hardcodes intended for silent host inject * Prefer tools-first loops; when LLM is required, assume measured digests only inside CVMs ## Package deterministically * `agent.py` at archive root with top-level `class Agent` * Compressed ZIP ≤ 1 MiB * Fixed timestamps so `zip_sha256` is stable; verify the submit receipt * No parent-path ZIP members ## Sign correctly * Sign the **challenge-local** path (`/submissions`), not the proxy prefix * Fresh nonce and timestamp every request * Run `python scripts/submit_agent.py selfcheck` offline first ## Self-deploy discipline * CPU TDX only; respect the money cap * Never print Phala or OpenRouter keys * Do not treat DB `review_allowed` alone as eval permission; wait for fresh allow materials * Tear down until `phala cvms list` total is 0 after every attempt ## Design for isolation Eval trials run under Docker-out-of-Docker style isolation. Task trees come from the baked guest cache (no network dataset fetch at eval). Do not depend on outbound network unless a task policy explicitly allows it. ## Related * [Quickstart](/challenges/agent-challenge/quickstart) * [Troubleshooting](/challenges/agent-challenge/troubleshooting) # Agent configuration Source: https://docs.joinbase.ai/challenges/agent-challenge/configuration Configure baseagent-style submissions for Agent Challenge without Base gateway embeds. Configuration has two layers: what you put in the ZIP for local/template defaults, and what the **production** scored path allows inside measured CVMs. ## Template runtime defaults The `baseagent` template keeps loop and compaction knobs in its defaults module (model name, max iterations, context budgets, cache). Treat those as **local development defaults**. Production allowlists and measurement pins on the challenge side determine which provider digests are legal inside review and eval guests. ## Production LLM policy (required) | Allowed on scored path | Forbidden | | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- | | Measured OpenRouter under review harness / `.rules` (keys via Phala `encrypted_env`) | Base LLM gateway (`BASE_LLM_GATEWAY_URL`, `BASE_GATEWAY_TOKEN`, `/llm/v1`) | | Measured OpenRouter digests inside eval guest when pin allows | Non-measured hard-coded provider keys and model emission strings in the ZIP | | Tools-only agents with no external LLM | Secrets in plain compose text or public status APIs | Continuous review and attestation gates reject Base gateway embeds and unauthorized provider configuration. See [Attestation](/challenges/agent-challenge/attestation-phala) and [Security residual notes](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/security.md). ## Miner env on legacy surfaces Historical host-side env PUT/confirm-empty exists on older or flag-off paths. On production TEE self-deploy, review and eval capability secrets enter through Phala `encrypted_env` that the self-deploy CLI builds, not through long-lived plain env blobs miners expect to re-read. ## Related * [baseagent](/challenges/agent-challenge/baseagent) * [Submit](/challenges/agent-challenge/submit) * [Best practices](/challenges/agent-challenge/best-practices) # Evaluation and scoring Source: https://docs.joinbase.ai/challenges/agent-challenge/evaluation Agent Challenge lifecycle: prepare, deploy, Terminal-Bench cache, score gate, and BASE weights. This page covers the submission lifecycle, score acceptance, and how results become BASE weights. ## Production path (mandatory) Production scoring requires: 1. `phala_attestation_enabled` / `CHALLENGE_PHALA_ATTESTATION_ENABLED` **ON** 2. `attested_review_enabled` **ON** 3. Miner-driven Phala CPU TDX **review** CVM, then (only after fresh re-verified allow) **eval** CVM 4. Attestation-only grading: measured OpenRouter under review `.rules`; **no** Base LLM gateway on the scored path 5. Direct `POST /evaluation/v1/runs/{eval_run_id}/result` with score-domain attestation and durable key-grant Validators do **not** deploy scored jobs for miners in production. Work-unit pull / `list_pending_work_units` style execution is legacy relative to the attested self-deploy path. ## High-level lifecycle 1. Miner signs and uploads an immutable ZIP (`POST /submissions`) 2. Digest becomes the stable agent hash; AST and similarity analysis may run as service gates 3. Attested **review** session: miner prepare/deploy measured review image; CVM produces domain-separated review report 4. Challenge verifies quote + review allowlist + review-domain `report_data` (bound times ≤24h) 5. Verdict: `allow` / `reject` / `escalate` 6. Miner **eval** prepare/deploy on separate measured **canonical** image (prepare refuses cache-only DB `review_allowed` bits) 7. Eval guest: GetTlsKey, RA-TLS golden key, trials from baked live-task-cache, attested RESULT 8. Accepted results may contribute to leaderboard and BASE raw weights Public clients poll `GET /submissions/{id}/status` or SSE `GET /submissions/{id}/events`. ## Prepare / deploy / key-release / score gate | Stage | What must hold | Fail closed when | | -------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------- | | Review prepare | Signed assignment; immutable ZIP digests | Signature / rate / capability failures | | Review deploy | Review allowlist `compose_hash`; Phala `encrypted_env` for OpenRouter + session | Missing encrypted\_env, GPU shape, money cap | | Review result | Review-domain quote + allowlist + bound times | Stale >24h, wrong domain, measurement mismatch | | Eval prepare | Fresh re-verified allow materials (not DB phase alone) | `review_allow_required`, stale allow, cached-allow-only refuse | | Eval deploy | Canonical compose\_hash + measurement; plan nonces | Plan/compose mismatch, OS pin drift | | Key release | Raw TCP TLS 1.3 + client cert + keyrelease-domain quote + allowlist | Deny returns no key; no L7 `/release` production path | | Score admit | Score-domain quote + event log + allowlist + durable key-grant + nonces | Missing key-grant or attestation materials write **no** score | ## Public phases (attested mode) Exact public strings can evolve. Conceptual map: | Concern | Phases / outcomes (illustrative) | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Review | `review_queued`, `review_cvm_running`, `review_provider_standby`, `review_verifying`, `review_allowed`, `review_rejected`, `review_escalated`, `review_expired`, `review_cancelled`, `review_error` | | Eval | `eval_prepared`, `eval_running`, `eval_verifying`, `eval_accepted`, `eval_rejected`, `eval_expired`, `eval_cancelled`, `eval_error` | | Pre-receipt failures | `eval_deploy_failed`, `eval_tunnel_failed`, `eval_key_release_unavailable`, `eval_no_result` | | Terminal public labels | `valid`, `invalid`, `suspicious`, `error` (and owner override forms where configured) | Review and eval history routes use stable cursor pagination (default 10, max 16) and retain cancelled, expired, failed, and superseding attempts. ## Terminal-Bench and the live task cache Production evaluation resolves Terminal-Bench task trees from a **measured guest path** (conceptually `/opt/agent-challenge/task-cache`). The canonical eval image bakes a live task cache so eval time does not network-fetch task definitions. * Prepare/select draw from the pinned digest fallback set when residual task-count paths apply * Incomplete bake surfaces as a guest task definition miss during preflight, **before** key release * Eval isolation uses Docker-out-of-Docker style trial containers inside the measured guest ## Scoring * Each selected task contributes a task score * Aggregate score is typically the average across selected tasks for a completed valid submission * Task selection is deterministic relative to the agent hash and the eval prepare plan * Defaults may use winner-take-all among valid submissions when configured; otherwise best score per miner hotkey * Only effective status `valid` or `overridden_valid` can produce weight entries and leaderboard rows * Timed-out tasks are terminal, non-passing, score 0, counted once * On the attested path, weight eligibility requires **verified** attestation acceptance including durable key-grant `GET /internal/v1/get_weights` is the challenge weight contract. BASE normalizes to UIDs (cross-repo). ## Acceptance checks (eval result) Before writing an accepted score the challenge verifies, in conjunction: * TDX quote integrity and acceptable TCB * Event log replay / compose identity * Measurement present on the **eval** allowlist * Score-domain `report_data` binds measurement, agent hash, task ids, scores digest, score nonce and eval\_run\_id * **Key-grant consistency** for that eval run (no grant, no score) * Nonces single-use / fresh * Fresh re-verified review allow at eval admit time Invalid or rejected results write no accepted score. ## Offline and flag-off With attestation flags **OFF**, the service may still run offline AST helpers and historical evaluation helpers for local CI. That mode: * must not be described as production scoring * does not require miners to spend Phala credits * keeps operators as challenge service runners, not substitutes for miner TEE self-deploy ## Related * [Attestation](/challenges/agent-challenge/attestation-phala) * [Key release](/challenges/agent-challenge/key-release) * [Evaluation (repo)](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/evaluation.md) # Agent Challenge Source: https://docs.joinbase.ai/challenges/agent-challenge/index Miner home for Agent Challenge: package an agent, self-deploy on Phala Intel TDX, and earn weights from attested Terminal-Bench scores. Agent Challenge is a primary challenge on BASE (netuid 100). Miners build software-engineering agents, submit a signed ZIP, then **self-deploy** measured Phala Intel TDX CVMs for attested review and evaluation. BASE is the subnet coordinator (proxy, registry, weight normalize). It is not a generic "mine Base" application surface. Production scoring is **attestation-only**: * Miner funds and operates review then eval CVMs on Phala **CPU TDX** * Challenge and validator config own dual measurement allowlists, RA-TLS golden AES-256 key release, and score acceptance * Review uses measured OpenRouter under challenge `.rules` (no Base LLM gateway on the scored path) * Eval decrypts golden task material only after RA-TLS key grant, runs Terminal-Bench from a baked guest task cache, and posts an attested result Trust is cryptographically-anchored **trust-but-audit**, not absolute TEE immunity. ```mermaid theme={"dark"} flowchart TB subgraph Miner ZIP[Signed ZIP submit] CLI[Self-deploy CLI] RCVM[Review CVM CPU TDX] ECVM[Eval CVM CPU TDX] end subgraph Challenge[Agent Challenge] API[API and signed routes] RV[Review verify] ALL[Dual measurement allowlists] KR["RA-TLS key release :8701"] DR[Direct result admissions] W[get_weights] end subgraph BASE[BASE hub] PX[Public proxy] AGG[Weight normalize] end ZIP --> PX PX --> API CLI --> RCVM RCVM -->|review report plus quote| API API --> RV RV -->|fresh allow only| CLI CLI --> ECVM ECVM -->|GetTlsKey client cert| ECVM ECVM -->|raw TCP mTLS| KR ALL --> KR ALL --> DR ECVM -->|POST attested result| DR DR --> W W --> AGG ``` ## Who does what | Role | Responsibility | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | **Miner** | Package agent, sign submit, fund Phala TDX CVMs, ordered review then eval, teardown, watch public status | | **Challenge operator** | Measurement allowlists, RA-TLS key release, quote verification, production flags, golden key custody (challenge-side, not the BASE validator mint) | | **BASE validator** | Runs BASE subnet infra (master, weights, on-chain submit). Does **not** deploy your TEE score jobs | ## Miner path Zero to first signed submit, then self-deploy. Package the ZIP and sign the upload. Dual images, report\_data domains, GetTlsKey, residual risk. Prepare, deploy, Terminal-Bench cache, score gate, weights. RA-TLS on :8701, golden key, client-trust vs server CA. Common fail-closed phases and fix paths. ## Build agents (template) Submission packaging and the `baseagent` Harbor entrypoint live under this challenge. Start with [baseagent](/challenges/agent-challenge/baseagent) and [quickstart](/challenges/agent-challenge/quickstart). ## Deep guides (repository) | Guide | Repository path | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | Architecture | [docs/architecture.md](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/architecture.md) | | Miner self-deploy | [docs/miner/self-deploy.md](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/miner/self-deploy.md) | | Attestation TEE | [docs/miner/attestation-tee.md](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/miner/attestation-tee.md) | | Evaluation | [docs/evaluation.md](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/evaluation.md) | | Security | [docs/security.md](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/security.md) | | Operator self-deploy | [docs/validator/self-deploy.md](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/validator/self-deploy.md) | Repository: [`BaseIntelligence/agent-challenge`](https://github.com/BaseIntelligence/agent-challenge). ## Related * [All challenges](/challenges/overview) * [PRISM (miner pack)](/challenges/prism/overview) * [Miner hub (wallet + choose challenge)](/miners/overview) * [BASE validators (subnet operators)](/validators/overview) # RA-TLS golden key release Source: https://docs.joinbase.ai/challenges/agent-challenge/key-release How the eval guest obtains the AES-256 golden key over raw TLS 1.3 on :8701, and how client-trust differs from server CA. The golden key is a validator-held **AES-256** secret used for AES-256-GCM packaging of oracle / golden task material. Production release is **raw TLS 1.3** with client certificates and dstack RA-TLS extensions. HTTP `POST /release` is disabled in production (404). Do not place a public L7 terminator in front of the listener. Default bind is conceptual `127.0.0.1:8701`, externalized with an operator tunnel that preserves raw TCP and end-to-end client certificate identity. ## Why key release exists Eval guests must decrypt golden task material to run honest Terminal-Bench style checks. The key must not: * ship in miner-visible images * appear on public APIs or logs * release before the guest proves eval-image measurement and key-release-domain quote alliance ## Guest path (GetTlsKey) Inside the measured eval guest: 1. dstack **GetTlsKey** materializes mTLS client cert and key (for example under `/run/secrets/ra_tls/`) 2. Operator injects the **validator server CA** so the guest can verify the key-release listener (`CHALLENGE_PHALA_RA_TLS_SERVER_CA_PEM` / server CA file envs) 3. Guest dials the listener, presents client cert + key-release-domain quote 4. On allowlist match and checks, receives `key_b64`; on deny, no key The guest never invents the validator server root. ## Host path (two CA roles) Keep these distinct: | Material | Who installs | Purpose | | ----------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | | **Server identity** | Operator on KR host (`KEY_RELEASE_RA_TLS_CERT_FILE` / `KEY_RELEASE_RA_TLS_KEY_FILE`) | Listener server certificate | | **Client-trust CA** | Operator on KR host (`KEY_RELEASE_RA_TLS_CA_FILE`) | Server verifies **guest** client certificates (dstack guest issuer) | | **Server CA for guest** | Injected into eval guest at deploy (`CHALLENGE_PHALA_RA_TLS_SERVER_CA_*`) | Guest verifies the **validator** listener | Client-trust CA is **not** the same blob as server CA inject. Operators may harvest the guest **public-only** fullchain (leaf + intermediates, never private keys) from guest logs or a known export path for client-trust install when remote pull is unavailable. ## Server checks After TLS, the client sends a length-prefixed canonical JSON frame with `schema_version`, `eval_run_id`, `nonce`, `quote_hex`, and `event_log`. Server checks: 1. TLS peer certificate vs RA-TLS quote extensions and allowlisted **eval** measurement 2. Event log / dual-domain allowlist membership for the eval image 3. Nonce freshness and rate limits 4. SPKI digest binding (caller-supplied peer-identity headers are not trusted) On denial: no golden key, no score path. Typical env names (operator surface; not miner secrets): ```text theme={"dark"} KEY_RELEASE_RA_TLS_HOST KEY_RELEASE_RA_TLS_PORT KEY_RELEASE_RA_TLS_CERT_FILE KEY_RELEASE_RA_TLS_KEY_FILE KEY_RELEASE_RA_TLS_CA_FILE CHALLENGE_KEY_RELEASE_ALLOWLIST_FILE CHALLENGE_GOLDEN_KEY_FILE CHALLENGE_KEY_RELEASE_ACCEPTABLE_TCB CHALLENGE_KEY_RELEASE_NONCE_TTL_SECONDS ``` Guest dial: ```text theme={"dark"} CHALLENGE_PHALA_RA_TLS_CERT_FILE CHALLENGE_PHALA_RA_TLS_KEY_FILE CHALLENGE_PHALA_RA_TLS_CA_FILE CHALLENGE_PHALA_RA_TLS_SERVER_CA_PEM CHALLENGE_PHALA_RA_TLS_SERVER_CA_FILE ``` ## Domain bind Key-release quotes use domain tag `base-agent-challenge-keyrelease-v1` and bind `eval_run_id`, the key-release nonce, and the RA-TLS SPKI digest. A review quote cannot authorize key release. A key-release quote cannot authorize a score. ## Offline HTTP fixture Local health and nonce helpers may use an offline HTTP surface on `:8700`. That path is not production key release. ## Score conjunction Score admission requires durable **key-grant** for that eval run in addition to the score-domain quote and allowlist. Missing grant writes **no** accepted score. ## Related * [Attestation](/challenges/agent-challenge/attestation-phala) * [Evaluation](/challenges/agent-challenge/evaluation) * [Operator self-deploy (challenge repo)](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/validator/self-deploy.md) # Agent Challenge quickstart Source: https://docs.joinbase.ai/challenges/agent-challenge/quickstart Package a baseagent ZIP, submit with a signed hotkey, then self-deploy review and eval on Phala TDX. This is the shortest miner path for Agent Challenge. You will: 1. Register a hotkey on BASE (netuid 100) 2. Build an agent from `baseagent` 3. Sign and upload the ZIP 4. Drive attested review then eval on Phala CPU TDX 5. Tear down CVMs BASE install and dry-run weights are for **validators**. Miners compete inside this challenge, not against a Base-global miner binary. ## Prerequisites * Python 3.12+ and a Bittensor wallet hotkey registered on **netuid 100** * Clone of [`BaseIntelligence/agent-challenge`](https://github.com/BaseIntelligence/agent-challenge) (submit helper + self-deploy CLI) * Clone or fork of [`BaseIntelligence/baseagent`](https://github.com/BaseIntelligence/baseagent) for the ZIP entrypoint * Phala Cloud API key in the environment only: `PHALA_CLOUD_API_KEY` (never commit it) * OpenRouter key for the **review** CVM only (delivered via Phala `encrypted_env`, not plain compose) See [Wallet and registration](/miners/wallet-registration) if you still need a netuid 100 hotkey. ## Steps Root of the archive must expose `agent.py` with top-level `class Agent` (Harbor `agent:Agent`). ```bash theme={"dark"} # From agent-challenge repo python scripts/submit_agent.py build \ --agent-dir ./my-agent \ --out ./my-agent.zip ``` Constraints: compressed ZIP ≤ 1 MiB; no parent-path members; no Base LLM gateway embeds (`BASE_LLM_GATEWAY_URL`, `BASE_GATEWAY_TOKEN`, `/llm/v1`). ```bash theme={"dark"} python scripts/submit_agent.py submit \ --api-base https:///challenges/agent-challenge \ --agent-dir ./my-agent \ --name "my-agent" \ --hotkey-mnemonic "$MINER_HOTKEY_MNEMONIC" ``` Keep `submission_id` and verify `zip_sha256` in the receipt. Details: [Submit](/challenges/agent-challenge/submit). Production requires challenge flags ON (`phala_attestation_enabled` and `attested_review_enabled`). You fund the review CVM: ```bash theme={"dark"} python -m agent_challenge.selfdeploy review deploy \ --base-url https:// \ --submission-id \ --hotkey \ --auto-sign \ --openrouter-key-env OPENROUTER_API_KEY \ --review-instance-type tdx.small \ --eval-instance-type tdx.small \ --money-cap-usd 20 ``` CPU TDX only. GPU shapes are refused. Projected spend is capped (default **\$20** lifetime for review + eval). Poll public status or self-deploy `review result` / `review history`. Only a **fresh re-verified** `allow` unlocks eval prepare. `reject`, `escalate`, expiry, or attestation failure does not create benchmark work or a score. After allow: ```bash theme={"dark"} python -m agent_challenge.selfdeploy eval deploy \ --base-url https:// \ --submission-id \ --hotkey \ --auto-sign \ --eval-instance-type tdx.small \ --money-cap-usd 20 ``` The eval guest obtains GetTlsKey client material, dials RA-TLS key release (default `:8701`), runs Terminal-Bench from the baked task cache, and posts the attested result. See [Evaluation](/challenges/agent-challenge/evaluation) and [Key release](/challenges/agent-challenge/key-release). Always tear down after success or failure: ```bash theme={"dark"} python -m agent_challenge.selfdeploy review teardown ... python -m agent_challenge.selfdeploy eval teardown ... phala cvms list # total should be 0 ``` ## What "done" looks like | Outcome | Meaning | | --------------------------------------------------- | ---------------------------------------------------------------------------- | | Public phase terminal `valid` (or overridden valid) | Score accepted with full attestation conjunction including durable key-grant | | Leaderboard row | Only effective valid submissions with accepted scores | | Weights | Challenge `GET /internal/v1/get_weights`; BASE normalizes to UIDs on-chain | Upload alone is **not** a production score. ## Next Canonical string, headers, rate limits. Dual images and quote domains. Full CLI stages in the challenge repo. Fail-closed reason codes and fixes. # Submit an agent Source: https://docs.joinbase.ai/challenges/agent-challenge/submit Package the Agent Challenge ZIP, sign miner requests, and upload through the challenge or BASE proxy. This page covers packaging and the signed upload. Production scoring after upload is miner self-deploy on Phala TDX. Continue with [Quickstart](/challenges/agent-challenge/quickstart) and [Evaluation](/challenges/agent-challenge/evaluation). A packaging helper lives in the challenge repo: `scripts/submit_agent.py`. ## Prerequisites * Hotkey that will receive score credit (registered on netuid 100 for accepted production submit on the live proxy) * Python 3.12+ with a substrate keypair (Bittensor `Keypair` is common) * API base: either the challenge host or the BASE proxy base ending in `/challenges/agent-challenge` ## Build the agent Entrypoint contract: * `agent.py` at the **archive root**, defining top-level `class Agent` * Built from [`BaseIntelligence/baseagent`](https://github.com/BaseIntelligence/baseagent) * No Base LLM gateway embeds (`BASE_LLM_GATEWAY_URL`, `BASE_GATEWAY_TOKEN`, `/llm/v1`) * No non-measured provider secrets or hard-coded emission model names in the ZIP * Legal LLM path on production: measured OpenRouter under the review/eval CVM with digests, or tools-only agents Minimal shape: ```python theme={"dark"} class Agent: async def run(self, instruction, environment, context): return "Task completed" ``` Required ZIP layout: ```text theme={"dark"} my-agent.zip ├── agent.py # required root entrypoint, defines class Agent ├── src/ # optional support code ├── pyproject.toml # optional └── requirements.txt # optional ``` | Constraint | Failure | | --------------------------------------- | ----------------------------------------------- | | Compressed size ≤ 1048576 bytes (1 MiB) | HTTP `413` `zip_too_large` | | No `..` or absolute members | HTTP `400` `parent_path` | | Immutable storage by SHA-256 | HTTP `409` `duplicate_code_hash` for duplicates | ```bash theme={"dark"} python scripts/submit_agent.py build --agent-dir ./my-agent --out ./my-agent.zip ``` Archives are built with fixed member timestamps so the same source yields the same `zip_sha256`. ## Sign the request Headers on every signed miner request: ```http theme={"dark"} X-Hotkey: X-Signature: 0x X-Nonce: X-Timestamp: ``` Canonical string (newline-joined, sign these exact bytes): ```text theme={"dark"} {METHOD} {PATH_WITH_SORTED_QUERY} {X-TIMESTAMP} {X-NONCE} {SHA256_HEX_OF_RAW_BODY} ``` Rules: * Path is the **challenge-local** path (for example `/submissions`), with query keys sorted * When routing through the BASE proxy, sign the local path, not `/challenges/agent-challenge/...` * Body hash is SHA-256 of the exact raw body bytes (empty body is SHA-256 of `b""`) * Each `(hotkey, nonce)` pair is single-use (replay → HTTP `409`) * Accepted uploads are rate-limited per hotkey per window (Settings default **10800** seconds). Second accepted upload in-window → HTTP `429` `submission_rate_limited` with `next_allowed_at` Reference: ```python theme={"dark"} import hashlib from urllib.parse import parse_qsl, urlencode def canonical(method, path, query, timestamp, nonce, raw_body: bytes) -> str: sorted_query = ( f"{path}?{urlencode(sorted(parse_qsl(query, keep_blank_values=True)))}" if query else path ) return "\n".join([ method.upper(), sorted_query, timestamp, nonce, hashlib.sha256(raw_body).hexdigest(), ]) ``` ```bash theme={"dark"} python scripts/submit_agent.py selfcheck ``` ## POST /submissions ```json theme={"dark"} { "miner_hotkey": "5Abc...", "name": "my-agent", "artifact_zip_base64": "" } ``` Scoring hotkey comes from the signed header, not the body (`miner_hotkey` is informational). Success is HTTP `201` with a receipt. Verify `zip_sha256`, keep `submission_id`. ```bash theme={"dark"} python scripts/submit_agent.py submit \ --api-base https://base.example/challenges/agent-challenge \ --agent-dir ./my-agent --name "my-agent" \ --hotkey-mnemonic "$MINER_HOTKEY_MNEMONIC" ``` ## Track status ```bash theme={"dark"} curl '/submissions//status' curl -N '/submissions//events' ``` Public phases evolve with the service. On production attestation, expect review-oriented then eval-oriented phases rather than a host-only Terminal-Bench launch story. Conceptual map: | Concern | Example phases | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------- | | Review | `review_queued`, `review_cvm_running`, `review_verifying`, `review_allowed`, `review_rejected`, `review_escalated`, ... | | Eval | `eval_prepared`, `eval_running`, `eval_verifying`, `eval_accepted`, ... | | Terminal public labels | `valid`, `invalid`, `suspicious`, `error` (and owner override forms where configured) | Safe fields only: digests, phases, reason codes, timestamps. No source, raw quotes, tokens, or golden material. ## After upload 1. Drive review CVM stages with `python -m agent_challenge.selfdeploy review ...` 2. After verified allow, deploy eval CVM and post attested RESULT 3. Tear down until `phala cvms list` reports `total: 0` See [Attestation](/challenges/agent-challenge/attestation-phala) and the [self-deploy guide](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/miner/self-deploy.md). Legacy offline paths used a host-side env gate and worker-style launch. Those are **not** the production TEE score path. ## Proxy note (BASE) Public traffic often arrives as `/challenges/agent-challenge/...`. Direct RESULT ingest and internal capability routes are challenge-owned and are not BASE-public-proxied. BASE proxy may also expose bridge upload under `/v1/challenges/...` for subnet-level signature checks; on Agent Challenge production, prefer the challenge docs and self-deploy CLI matching your live endpoint. Subnet-level signing headers for generic proxy bridge uploads are documented in [Authentication](/miners/authentication) when you use the bridge path for other challenges. Always prefer the **challenge-local** canonical string for Agent Challenge signed routes above. # Tools & capabilities Source: https://docs.joinbase.ai/challenges/agent-challenge/tools The tool surface the baseagent template exposes, and how tools execute inside the Agent Challenge task workspace. A submitted agent acts on a task by calling tools. The `baseagent` template ships a tool registry covering file operations, search, execution, and media. Inside the Agent Challenge, those tools run through `environment.exec` in the remote task workspace. ## Available tools The `baseagent` template exposes these tools (`baseagent/README.md:204-213`): | Tool | Description | Key parameters | | --------------- | ---------------------------- | ------------------------------ | | `shell_command` | Execute shell commands | `command`, `timeout_ms` | | `read_file` | Read files with pagination | `file_path`, `offset`, `limit` | | `write_file` | Create/overwrite files | `file_path`, `content` | | `apply_patch` | Apply unified diff patches | `patch` | | `grep_files` | Search with ripgrep | `pattern`, `path`, `include` | | `list_dir` | List directory contents | `path`, `recursive`, `depth` | | `search_files` | Search files by glob pattern | `pattern`, `path` | | `view_image` | Analyze image files | `file_path` | These group into file operations (`read_file`, `write_file`, `apply_patch`), search and navigation (`grep_files`, `list_dir`, `search_files`), execution (`shell_command`), and media (`view_image`). (`baseagent/README.md:171-202`) ## How tools execute The tool registry validates arguments, checks a cache, and on a cache miss runs the tool implementation, caches the result, and returns it to the agent loop. (`baseagent/README.md:217-240`) Inside the Agent Challenge, Harbor execution uses `src/tools/harbor_registry.py` so task tools run through `environment.exec` in the remote task workspace. The default task working directory is `/app`; `/workspace/agent` is the mounted agent artifact, not the task filesystem. (`baseagent/README.md:104`) The challenge's reference entrypoint demonstrates the contract by running a single command via `environment.exec` to prove in-container execution (`agent-challenge/scripts/example_agent/agent.py:53-63`): ```python theme={"dark"} async def run(self, instruction, environment, context=None): result = await environment.exec( f"echo {EXECUTION_MARKER} | tee /tmp/{EXECUTION_MARKER}", env=self._extra_env or None, ) return (result.stdout or "").strip() or EXECUTION_MARKER ``` ## Tool output management Tool output is bounded so long tasks stay within the context budget. The template truncates tool output (`max_output_tokens`, \~10KB) and protects the most recent 40,000 tokens of tool output from pruning. (`baseagent/src/config/defaults.py:39-40,52`) Inside evaluation, task containers run `--network none` unless a task opts in, so design tools to work without outbound network access. (`agent-challenge/README.md:265`) ## Next steps How the agent loop drives these tools. Build reliable, reproducible agents. # Agent Challenge troubleshooting Source: https://docs.joinbase.ai/challenges/agent-challenge/troubleshooting Common submit, review, eval, and attestation failures for Agent Challenge miners. Most production failures are fail-closed by design. Prefer the public phase and bounded `reason_code` over guessing. ## Submit and signing | Symptom | Likely cause | What to try | | ------------------------------------ | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | HTTP `401` / signature invalid | Wrong canonical path, body hash, or clock skew | Sign challenge-local path; hash exact body; refresh timestamp within 300s; run `submit_agent.py selfcheck` | | HTTP `409` nonce | Nonce replay | New unique nonce per request | | HTTP `409` `duplicate_code_hash` | Same ZIP already stored | Change artifact content or fix packaging | | HTTP `413` `zip_too_large` | ZIP > 1 MiB compressed | Shrink artifact | | HTTP `429` `submission_rate_limited` | Second accepted upload in rate window | Honor `next_allowed_at` | ## Review stage | Symptom | Likely cause | What to try | | ------------------------------------ | ------------------------------------------------------ | --------------------------------------------------------------------------------- | | GPU / shape refused | Non-CPU TDX shape | Use `tdx.small` / `tdx.medium` only | | Money cap refuse | Projected cost over cap | Lower runtime hours or raise only if you accept higher spend; default cap is \$20 | | Missing `encrypted_env` | OpenRouter or review session not encrypted into create | Use CLI `review deploy` so secrets go through Phala encrypted\_env only | | `review_rejected` | Policy or harness reject | Read public report digests; fix agent / provider embeds; resubmit as new version | | `review_escalated` | Owner review pause | Wait for owner; do not expect silent auto-eval | | Allow but eval prepare still refuses | Cache-only DB phase | Prepare needs **fresh re-verified** allow materials, not phase bits alone | ## Eval and key release | Symptom | Likely cause | What to try | | --------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------- | | `eval_key_release_unavailable` / tunnel | Cannot reach RA-TLS listener | Operator tunnel must preserve raw TCP and client certs; no L7 terminator | | Deny with no key | Measurement/allowlist/domain/nonce failure | Compare self-deploy measurements to published dual allowlists; fix pin mismatch | | Preflight task miss | Incomplete live-task-cache bake (operator image issue) | Report to challenge operators; miners cannot backfill opposite-side bake | | RESULT accepted nowhere | Missing key-grant or score-domain check | Ensure key release succeeded for this `eval_run_id` before RESULT | | Leftover Phala cost | Missed teardown | Run review/eval teardown; confirm `phala cvms list` total 0 | ## Policy embeds Submissions that embed Base gateway material or non-measured provider secrets fail review: * Forbidden: `BASE_LLM_GATEWAY_URL`, `BASE_GATEWAY_TOKEN`, `/llm/v1` * Production legal LLM: measured OpenRouter under harness digests, or tools-only ## Status does not mean weight Public `valid` and weight eligibility still require verified attestation acceptance (including durable key-grant on the TEE path). Challenger-reported numbers alone never become weights. ## Where to dig deeper * [Submit](/challenges/agent-challenge/submit) * [Attestation](/challenges/agent-challenge/attestation-phala) * [Key release](/challenges/agent-challenge/key-release) * [Self-deploy (repo)](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/miner/self-deploy.md) * [Security residual risk (repo)](https://github.com/BaseIntelligence/agent-challenge/blob/main/docs/security.md) # Agent SWE Challenge Source: https://docs.joinbase.ai/challenges/agent-swe Miner-facing guide for Agent SWE: real repositories as fail-to-pass software engineering benchmarks. Agent SWE turns real repositories into benchmark tasks for autonomous software-engineering agents. It preserves the hardness of real code work: existing project structure, real tests, install commands, patches, Docker evaluation, and a clear fail-to-pass scoring contract. Tasks may come from real pull requests or from a synthetic feature-deletion pipeline. **Status:** Secondary / experimental relative to Agent Challenge and PRISM. Confirm live slug and emission share via the registry. ## What it is | Item | Detail | | -------------- | --------------------------------------------------------------------------------------- | | Slug (typical) | `agent-swe` | | Artifact | Agent package (challenge-defined, often Tool + Harbor style similar sibling challenges) | | Scoring | Fail-to-pass on real tests inside Docker evaluation | | Good for | Agents that edit real repos under install and test friction | ## Miner path Hotkey on netuid 100. See [Wallet and registration](/miners/wallet-registration). Clone [`BaseIntelligence/Agent-SWE`](https://github.com/BaseIntelligence/Agent-SWE) and follow its miner documentation for entrypoint, ZIP layout, and runtime policy. Reproduce a single task container if the repo provides fixtures. Prefer agents that install deps, edit carefully, and re-run tests. Use challenge-signed submit under `/challenges/agent-swe/...` (exact path in repo). Subnet authentication patterns: [Authentication](/miners/authentication). Poll challenge status endpoints. Scores become challenge raw weights; BASE normalizes as usual. ## Relationship to Agent Challenge Agent Challenge (primary) uses **Phala TDX self-deploy** and Terminal-Bench oriented attested scoring. Agent SWE is a **separate** challenge with its own eval surface. Do not assume the same TEE money model, key-release flow, or env inject path unless this repository documents them explicitly. ## Repository [`BaseIntelligence/Agent-SWE`](https://github.com/BaseIntelligence/Agent-SWE) ## Related * [Agent Challenge (primary)](/challenges/agent-challenge) * [All challenges](/challenges/overview) * [Miner hub](/miners/overview) # Bounty Challenge Source: https://docs.joinbase.ai/challenges/bounty-challenge Miner-facing guide for Bounty Challenge: owner-set project briefs, GitHub submissions, human review, emissions. Bounty Challenge is a secondary challenge on BASE for open-ended project work that needs **human judgment**. An owner publishes a brief, miners submit finished work (typically as GitHub links), and the owner reviews and assigns emissions to hotkeys that produced the best results. The subnet then normalizes those challenge outputs into netuid 100 weights. **Status:** Secondary / experimental relative to Agent Challenge and PRISM. Confirm the live slug and emission share via the registry before registering spend. ## What it is | Item | Detail | | -------------- | ---------------------------------------------------------------------------------- | | Slug (typical) | `bounty-challenge` | | Artifact | Finished project work, usually linked from a submission record (challenge-defined) | | Scoring | Owner review assigns emissions; not a single automatic unit test score | | Good for | Product flows, UX work, open-ended builds where auto-graders are insufficient | ## Miner path Hotkey on netuid 100. See [Wallet and registration](/miners/wallet-registration). Use the challenge public routes under `/challenges/bounty-challenge/...` (exact paths in the challenge repository). List live challenges: ```bash theme={"dark"} curl -sf "$PROXY_URL/v1/registry" ``` Implement the owner brief in a repository the challenge accepts (commonly a public GitHub URL plus metadata). Follow the challenge signing and submit contract. Identity and subnet proxy signing patterns: [Authentication](/miners/authentication). Track owner review status on challenge endpoints, not only the subnet layer. There is no Base-global "run miner" binary for bounties. Submission format and review UX are **challenge-owned**. ## Costs and expectations * Chain registration and wallet security are miner-side * Compute spends (if any) depend on the bounty, not a subnet TEE mandate * Owner discretion means timelines and feedback differ from automatic challenges ## Repository [`BaseIntelligence/bounty-challenge`](https://github.com/BaseIntelligence/bounty-challenge) ## Related * [All challenges](/challenges/overview) * [Data Fabrication](/challenges/data-fabrication) * [Miner hub](/miners/overview) # Creating a challenge Source: https://docs.joinbase.ai/challenges/creating Scaffold a new challenge repository from the BASE template and wire it into the subnet. A challenge is an independent repository and Docker image. It owns its logic, public routes, submissions, scoring data, database schema, and challenge-local files. This page covers scaffolding a new challenge and registering it with the subnet. ## Scaffold the repository Generate a new challenge repository from the template with the challenge CLI: ```bash theme={"dark"} uv run platform challenge create code-arena --out ../code-arena cd ../code-arena uv run --extra dev pytest ``` The `create` command renders the challenge template into the destination path and reports how many files were written. You can pass `--name`, `--image`, and `--version` to override the defaults, and `--overwrite` to replace existing files. The template is rendered from a `ChallengeTemplateContext` built from the slug: the slug becomes the package name, display name, and the GHCR image reference for the new repository. ## Implement the weight contract In the generated repository, implement the weight function the subnet reads: ```python theme={"dark"} async def get_weights() -> dict[str, float]: return {"5F...hotkey": 1.0} ``` The master normalizes returned values, so raw scores are acceptable as long as they are finite and non-negative. ## Required API Every challenge must expose the same minimal contract: ```text theme={"dark"} GET /health GET /version GET /internal/v1/get_weights ``` The internal endpoint is authenticated with a per-challenge shared token mounted by the master. Public routes are exposed through `/challenges/{slug}/...`. ## Database and storage Generated challenges use the async SQLAlchemy SDK and read their runtime database URL from `CHALLENGE_DATABASE_URL`. The runtime is SQLite-backed; the subnet injects a URL pointing at the SQLite file on the challenge `/data` Swarm volume: ```text theme={"dark"} sqlite+aiosqlite:////data/challenge.sqlite3 ``` Use `/data` for the SQLite database, artifacts, analyzer output, uploaded files, and any local state that should survive restarts. It is the only persistent store for a challenge, and it is retained by default when a challenge service is removed. Challenges must never receive central control-plane PostgreSQL credentials. The shared control-plane database is only for master and validator state. ## Register and run Once the image is built and published, register and activate the challenge through the admin CLI: ```bash theme={"dark"} uv run platform challenge register code-arena ghcr.io/.../code-arena:latest 0.1 uv run platform challenge activate code-arena ``` The generated CI workflow tests the challenge and pushes its Docker image to GHCR on main and tags. ## Next The shared challenge-side helpers used by generated challenges. ## Related How the master reads weights and routes traffic to a challenge. Full reference for `platform challenge` commands. *** Sources: `base/docs/challenges.md` (model, required API, create command), `base/docs/challenge-integration.md` (`get_weights`, database contract, storage), `base/src/platform_network/cli_app/main.py:857` (`challenge create`) and `:873` (`challenge register`), `base/src/platform_network/template_engine.py:13` (`ChallengeTemplateContext`) and `:62` (`render_challenge_template`). # Data Fabrication Challenge Source: https://docs.joinbase.ai/challenges/data-fabrication Miner-facing guide for Data Fabrication: submit dataset-generation harnesses, quality over volume. Data Fabrication is a secondary challenge on BASE that rewards miners who generate useful **agentic coding conversation datasets**. You submit a complete dataset-generation harness. The challenge executes and reviews it, then rewards hotkeys that produce high-quality, diverse, verifiable, and original examples. **Status:** Secondary / experimental relative to Agent Challenge and PRISM. Confirm live registry entry and emission share before large spends. ## What it is | Item | Detail | | -------------- | -------------------------------------------------------------- | | Slug (typical) | `data-fabrication` | | Artifact | Dataset-generation harness (challenge-defined package) | | Success signal | Quality, diversity, verifiability, originality over raw volume | | Evaluation | Challenge-owned execution and review pipeline | ## Miner path Hotkey on netuid 100. See [Wallet and registration](/miners/wallet-registration). Clone [`BaseIntelligence/data-fabrication`](https://github.com/BaseIntelligence/data-fabrication) and follow its miner README for entrypoints, allowed deps, and packaging. Prefer verifiable and diverse conversation traces. Avoid trivial duplication and ungrounded synthetic soup. Use signed routes under `/challenges/data-fabrication/...` (or bridge upload if the live proxy documents it). Signing patterns: [Authentication](/miners/authentication). Track challenge status and leaderboard. Accepted raw weights flow through BASE normalization like every other challenge. ## Costs and TEE This challenge does **not** use the Agent Challenge Phala TDX self-deploy path. Cost model (CPU, storage, any external APIs the harness needs) is defined by the challenge and your harness; keep API secrets out of the public artifact when the contract forbids them. ## Repository [`BaseIntelligence/data-fabrication`](https://github.com/BaseIntelligence/data-fabrication) ## Related * [All challenges](/challenges/overview) * [Bounty Challenge](/challenges/bounty-challenge) * [Agent SWE](/challenges/agent-swe) # Challenges on BASE Source: https://docs.joinbase.ai/challenges/overview Challenges are where miners compete. BASE validators run subnet infrastructure; each challenge owns its miner path. BASE is a multi-challenge Bittensor subnet (netuid 100). **Validators run BASE** (master, weights, join, wallet as validator). **Miners compete inside challenges**. There is no undirected "mine Base" game path: each challenge owns its submissions, evaluation model, costs, TEE requirements (if any), and CLI or API. Every challenge exposes the same minimal weight contract to the subnet: ```text theme={"dark"} GET /health GET /version GET /internal/v1/get_weights ``` The internal weight endpoint is authenticated with a per-challenge shared token mounted by the master. Public traffic reaches a challenge through `/challenges/{slug}/...`. ```mermaid theme={"dark"} flowchart LR M[Miners] --> P[Choose challenge] P --> AC[Agent Challenge] P --> PR[PRISM] P --> O[Other challenges] AC --> W[Raw hotkey weights] PR --> W O --> W W --> B[BASE normalize] B --> V[Validators on-chain] ``` ## Primary challenges (full miner packs) Software-engineering agents; Phala TDX self-deploy; RA-TLS key release; Terminal-Bench eval. Architecture + training scripts scored on learn-from-scratch compression. ## Additional challenges Solid miner-facing overviews. Confirm live slug and status via the registry before spending chain fees. Owner-reviewed project bounties that need human judgment. Generate diverse, high-quality agentic coding datasets. Real-code software engineering benchmarks for agents. ## Build a challenge (authors) Scaffold a new challenge repository and wire it into the subnet. Shared challenge-side helpers for evaluation and weights. ## Related * [Miner hub](/miners/overview) (wallet + choose challenge) * [BASE validators](/validators/overview) (subnet operators only) # PRISM Challenge Source: https://docs.joinbase.ai/challenges/prism Hub stub: full PRISM miner documentation lives under Challenges → PRISM. PRISM is a primary challenge on BASE. Miners submit two scripts (`architecture.py` and `training.py`). The challenge re-executes training under forced random init and scores learn-from-scratch compression. Full miner pack: Overview, how it works, quickstart, submit, scoring, constraints, examples, API, and challenge operators. ## One-line flow 1. Submit two-script bundle through the PRISM challenge routes 2. Static AST sandbox and LLM hard gate 3. GPU re-execution on locked FineWeb-Edu train split 4. Challenge-computed prequential bits-per-byte score 5. Raw weights via `get_weights`; BASE normalizes for netuid 100 [Agent Challenge](/challenges/agent-challenge) is the other primary miner pack. [All challenges](/challenges/overview). # PRISM API Source: https://docs.joinbase.ai/challenges/prism/api The PRISM HTTP API: public challenge routes for submissions and the leaderboard, and the internal bridge, worker, and weights routes. PRISM exposes **public** challenge routes and **internal** routes. The public routes are mounted under the `/v1` prefix; the internal routes are mounted under `/internal/v1` and require internal authentication. *Source: `src/prism_challenge/routes.py:25`; `src/prism_challenge/app.py:64-72`; `docs/api.md:1-3`.* **Internal routes are not for miners.** Every route under `/internal/v1` requires internal authentication (`Authorization: Bearer `) and is called by the master, the proxy, or the worker - never directly by a miner. They are clearly marked **INTERNAL** below. ## Public routes ### `POST /v1/submissions` Submit a two-script bundle directly to PRISM. The direct public route uses miner authentication headers (`auth_required=True`, resolved by `authenticate_miner`). In production, miner submissions usually enter through the proxy, which uses the internal bridge route instead. ```json theme={"dark"} { "filename": "project.zip", "code": "", "metadata": {} } ``` *Source: `src/prism_challenge/routes.py:32-52`; `docs/api.md:15-31`.* ### `GET /v1/submissions/history` Returns daily submission counts over a window (`days`, default 90). *Source: `src/prism_challenge/routes.py:55-67`.* ### `GET /v1/submissions/{submission_id}` Returns status and score fields: ```json theme={"dark"} { "id": "...", "hotkey": "...", "epoch_id": 123, "status": "completed", "code_hash": "...", "created_at": "...", "error": null, "final_score": 0.72, "anti_cheat_multiplier": 1.0 } ``` `final_score` is the challenge-computed prequential bits-per-byte score (a lower bpb yields a higher `final_score`). `status` can be `pending`, `running`, `completed`, `failed`, `rejected`, or `held`. The response also carries `q_arch`, `q_recipe`, `diversity_bonus`, and `penalty` as **legacy fields** retained for response-schema stability; the live scoring path populates `final_score`. *Source: `src/prism_challenge/routes.py:70-78`; `docs/api.md:37-61`.* ### `GET /v1/leaderboard` Returns submissions ranked by `final_score` for the current epoch (earliest-commit-wins on a tie, one entry per hotkey). Optional `epoch_id` query. *Source: `src/prism_challenge/routes.py:81-103`.* ### `GET /v1/architectures` **Legacy** family-listing endpoint retained for API compatibility (`limit`, default 50). *Source: `src/prism_challenge/routes.py:106-123`; `docs/api.md:68-70`.* ### `GET /v1/training-variants` **Legacy** variant-listing endpoint retained for API compatibility. Optional query parameters: `architecture_id`, `limit`. *Source: `src/prism_challenge/routes.py:126-151`; `docs/api.md:72-75`.* ### `GET /v1/epochs/current` and `GET /v1/epochs` Return the current epoch id and length, and recent epochs (`limit`, default 50). *Source: `src/prism_challenge/routes.py:154-175`.* ### `GET /v1/health/eval-jobs` Returns recent eval-job health entries (id, submission id, level, status, attempts). *Source: `src/prism_challenge/routes.py:178-195`.* ### `GET /v1/gpu/status` Returns a GPU-lease summary (total GPUs, active leases, by status, by tier). *Source: `src/prism_challenge/routes.py:198-219`.* ## Internal routes All internal routes require `Authorization: Bearer ` and are gated by `authenticate_internal`. They are part of the subnet integration surface, **not** the miner API. ### `POST /internal/v1/bridge/submissions` - INTERNAL Receives subnet-verified submissions forwarded by the proxy. Gated by `authenticate_internal`. The verified hotkey is supplied by the trusted `X-Platform-Verified-Hotkey` header; miner-supplied identity headers are not trusted. The body can be raw ZIP bytes or JSON matching `SubmissionCreate`. ```text theme={"dark"} Authorization: Bearer X-Platform-Verified-Hotkey: X-Submission-Filename: project.zip Content-Type: application/zip ``` *Source: `src/prism_challenge/app.py:68-86`; `_bridge_submission_create` `app.py:91-107`; `docs/api.md:106-119`.* ### `POST /internal/v1/worker/process-next` - INTERNAL Claims and processes one pending submission through the full pipeline: the static gates, the OpenRouter LLM hard gate, the forced-init re-execution, and prequential bits-per-byte scoring. Gated by `authenticate_internal`. *Source: `src/prism_challenge/app.py:64-66`; `docs/api.md:121-124`.* ### `GET /internal/v1/get_weights` - INTERNAL The standard challenge contract route. Returns normalized, dry-run hotkey weights (one per hotkey, from that hotkey's best `final_score`). Weights are never written on-chain. The handler is wired into the challenge app via `get_weights_fn`. *Source: `src/prism_challenge/app.py:44-57`; `src/prism_challenge/weights.py:9-31`; `docs/api.md:101-104`.* ## Service routes `GET /health` returns challenge health metadata, and `GET /version` returns the challenge version, API version, SDK version, and capabilities. *Source: `docs/api.md:7-13`.* See [Running PRISM](/challenges/prism/operators) for authentication configuration and deployment. # Constraints & limits Source: https://docs.joinbase.ai/challenges/prism/constraints The PRISM submission limits: the two-script contract, the 150M parameter cap, the AST sandbox, locked data with no network, and the single-node GPU bounds. PRISM fixes the dataset and the evaluation protocol, not the model search space - but every submission must stay inside a fixed set of contract, sandbox, and resource limits. A violation is rejected at static review, before any GPU work. *Source: `docs/submissions.md:1-10`.* ## Two-script contract A bundle must contain two **distinct** scripts: an architecture role exposing `build_model` and a training role exposing `train`. The single-module re-export idiom no longer satisfies the contract - if the architecture and training entrypoints resolve to the same file, the submission is rejected. *Source: `src/prism_challenge/evaluator/components.py:99-103`.* ## Parameter cap The realized model is capped at **150M parameters** (`max_parameters = 150_000_000`). The cap is enforced statically at forced-seed instantiation and re-checked inside the container against the model the runner actually trained. *Source: `src/prism_challenge/evaluator/interface.py:26`; `src/prism_challenge/evaluator/container.py:1149-1176`; `docs/submissions.md:6`.* ## Token geometry The context exposes a default token-id geometry the model must respect: `vocab_size = 4096` and `sequence_length = 128` (`max_seq_len`). *Source: `src/prism_challenge/evaluator/interface.py:23-24`.* ## AST sandbox Both scripts pass an AST sandbox of hard blocks before any GPU work. Unsafe imports, network access, arbitrary filesystem access, and deserialization escapes are rejected. `build_model` must stay pure: it must not read data, open files, touch the network, or reference the dataset. *Source: `docs/miner/README.md:39-41`, `:124-125`; `docs/submissions.md:24-26`.* ## Locked data, no network The train split is exposed read-only at `ctx.data_dir`; the `val`/`test` splits are secret and never exposed to your script. The eval container runs with `network=none`, `HF_HUB_OFFLINE=1`, and `HF_DATASETS_OFFLINE=1`, so there is no network during training. Read raw text from `ctx.data_dir`, tokenize with your own tokenizer or a pre-staged reference, and fail closed if the locked data is missing rather than fabricating data. *Source: `docs/submissions.md:83-94`; `docs/miner/README.md:76-81`.* ## Single-node multi-GPU bounds PRISM is **single-node** only. Runs use 1-8 GPUs on one node, and the official scored run uses `torchrun --standalone --nnodes=1 --nproc-per-node=1` (the `nproc=1` path, since one physical GPU exists). Requests above 8 GPUs or for multiple nodes are rejected. A correct `training.py`: * calls `init_process_group` (nccl on GPU) and `set_device(local_rank)`; * wraps the model with DDP or FSDP and shards data per-rank; * does rank-0-only logging and artifact writes; * all-reduces any reported metrics, then `barrier()` and `destroy_process_group()` on exit; * also works correctly at `world_size=1`. Multi-GPU correctness is validated off the single physical GPU with a static contract check and a gloo multi-rank functional test (world size 2 and 4 on CPU). True 8-GPU scaling is an accepted, unverifiable limitation on a one-GPU node. *Source: `docs/submissions.md:96-115`; `docs/scaling.md:22-46`.* ## Writable paths `ctx.artifacts_dir` is the only writable path, and only rank 0 writes. The eval container is non-root with a read-only rootfs except `artifacts_dir`. *Source: `docs/submissions.md:66`; `docs/architecture.md:100-102`.* ## Compute budget The score is compute-normalized; wall-clock is only a safety cap, enforced in layers - a graceful budget that stops the loop and scores the partial stream, a hard watchdog above it, and an outer broker timeout. A faster or larger GPU configuration does not change the ranking; it only changes how much of the budget the run can use. *Source: `docs/scaling.md:48-59`.* ## Size and archive limits | Limit | Value | Source | | ---------------------------- | -------------------------------- | ----------------------------- | | Max submission code size | `max_code_bytes = 200000` | `config.example.yaml:12` | | Epoch length | `epoch_seconds = 21600` | `config.example.yaml:16` | | ZIP path traversal | rejected | `docs/submissions.md:173-183` | | ZIP symlinks | rejected | `docs/submissions.md:173-183` | | ZIP file count / total bytes | bounded | `docs/submissions.md:173-183` | | Allowed suffixes | approved text/code suffixes only | `docs/submissions.md:173-183` | ZIP submissions are extracted defensively. Unsupported or unsafe archives are rejected before evaluation. ## LLM hard gate A strong OpenRouter LLM reviews both scripts as a hard gate and can reject before any GPU work. A `reject` is terminal. *Source: `README.md:55`; `docs/miner/README.md:126`.* See [Submitting to PRISM](/challenges/prism/submit) for the manifest and [Scoring](/challenges/prism/scoring) for how a valid run is scored. # Providing context Source: https://docs.joinbase.ai/challenges/prism/context The PrismContext object your architecture and training scripts receive, including geometry, limits, the locked data path, and distributed launch fields. Both of your scripts receive a single `PrismContext` (`ctx`). It supplies the metadata and limits your model and loop need, and it controls everything the miner must not: the dataset, the seed, the scoring, and the held-out evaluation. *Source: `docs/submissions.md:56-74`; `src/prism_challenge/evaluator/interface.py`.* ## PrismContext fields and methods | Field / method | Meaning | Source | | -------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | `vocab_size`, `max_seq_len` | Token-id geometry for the model | `src/prism_challenge/evaluator/interface.py:23-24`, `:49-50` | | `max_params` | Hard parameter cap (150M) | `src/prism_challenge/evaluator/interface.py:26`, `:53-54` | | `seed` | The forced seed (challenge-controlled; you cannot change it) | `docs/submissions.md:64` | | `data_dir` | Read-only path to the locked FineWeb-Edu **train** split | `docs/submissions.md:65` | | `artifacts_dir` | The only writable path (rank-0 writes) | `docs/submissions.md:66` | | `device`, `world_size`, `rank`, `local_rank` | Distributed launch geometry | `docs/submissions.md:67` | | `token_budget`, `step_budget` | Compute budget for the run | `docs/submissions.md:68` | | `build_model()` | Helper that builds the model from `architecture.py` | `docs/submissions.md:69` | | `reference_tokenizer(name)` | Loads a pre-staged offline tokenizer (`"gpt2"` or `"llama"`); never touches the network | `src/prism_challenge/evaluator/interface.py:56-66` | *Source: `docs/submissions.md:60-71`.* ## What you control - and what you do not You provide model code and a training loop, not your own data. PRISM supplies and controls the dataset. The miner does **not** control: * the dataset content or splits; * the seed and initialization (forced by the harness); * the scoring; * the held-out evaluation. *Source: `docs/submissions.md:72-73`; `docs/miner/README.md:72-74`.* ## Reading the locked data Read raw text from `ctx.data_dir` and tokenize it with your own tokenizer or a pre-staged reference. The `val`/`test` splits are secret and never exposed to your script - only the challenge scorer reads them. The eval container runs with `network=none`, `HF_HUB_OFFLINE=1`, and `HF_DATASETS_OFFLINE=1`, so there is no network during training. Do not try to download data, tokenizers, or weights at runtime. **Fail closed if the locked data is missing** rather than fabricating data. *Source: `docs/miner/README.md:76-81`; `docs/submissions.md:91-94`.* ## Reference tokenizers `ctx.reference_tokenizer(name)` loads a pre-staged tokenizer entirely offline. Two references are available: `"gpt2"` (via a tiktoken cache) and `"llama"` (via a sentencepiece `.model`). Using a reference tokenizer never touches the network. ```python theme={"dark"} def train(ctx): tok = ctx.reference_tokenizer("gpt2") ... ``` *Source: `src/prism_challenge/evaluator/interface.py:56-66`; `docs/miner/README.md:70`.* Because the score normalizes by raw UTF-8 bytes, the metric is tokenizer-agnostic - you can bring any tokenizer and still be compared like for like. See [Scoring](/challenges/prism/scoring). ## Distributed geometry `ctx.world_size`, `ctx.rank`, `ctx.local_rank`, and `ctx.device` describe the launch shape. The harness launches `torchrun --standalone --nnodes=1 --nproc-per-node=` and exposes `WORLD_SIZE`, `RANK`, and `LOCAL_RANK`. Your loop must also work correctly at `world_size=1`, because the official scored run uses one physical GPU. *Source: `docs/submissions.md:96-110`; `docs/scaling.md:22-35`.* See [Constraints](/challenges/prism/constraints) for the sandbox, the parameter cap, and the multi-GPU bounds. # Examples Source: https://docs.joinbase.ai/challenges/prism/examples A complete, runnable two-script PRISM bundle: the tiny ~1M-parameter example with architecture.py, training.py, and prism.yaml. This page walks through the minimal, valid PRISM submission shipped in the source repository: a weight-tied \~1.05M-parameter decoder transformer split into the two-script contract. *Source: `examples/tiny-1m/README.md:1-13`.* ## Layout ```text theme={"dark"} examples/tiny-1m/ prism.yaml # declares the architecture + training entrypoints and the tokenizer architecture.py # exposes build_model(ctx); defines the model only training.py # exposes train(ctx); the miner-owned loop ``` * `architecture.py` exposes `build_model(ctx)` and is pure: it never reads data, opens files, or touches the network. * `training.py` exposes `train(ctx)`: it forces the seed, builds the model via `architecture.py`, reads the read-only locked train split from `ctx.data_dir`, tokenizes with the pre-staged gpt2 reference tokenizer (offline), runs a single-node multi-GPU-safe loop, and writes only under `ctx.artifacts_dir`. *Source: `examples/tiny-1m/README.md:6-20`.* ## The manifest ```yaml theme={"dark"} architecture: entrypoint: architecture.py training: entrypoint: training.py tokenizer: gpt2 ``` This manifest omits `kind`, so the submission defaults to the `full` mode. See [Submitting to PRISM](/challenges/prism/submit) for the three modes. *Source: `examples/tiny-1m/prism.yaml:1-5`; `src/prism_challenge/evaluator/components.py:22`.* ## A minimal architecture ```python theme={"dark"} import torch class TinyModel(torch.nn.Module): def __init__(self, vocab_size): super().__init__() self.embedding = torch.nn.Embedding(vocab_size, 8) self.linear = torch.nn.Linear(8, vocab_size) def forward(self, tokens): return self.linear(self.embedding(tokens)) def build_model(ctx): return TinyModel(ctx.vocab_size) ``` *Source: `docs/submissions.md:137-153`.* ## A minimal training entrypoint ```python theme={"dark"} from architecture import build_model def train(ctx): model = build_model(ctx) # construct the optimizer/schedule, read ctx.data_dir, tokenize, run the loop, # handle multi-GPU, and write only under ctx.artifacts_dir. ... ``` The container resolves `architecture.py::build_model` and `training.py::train`, forces the seed, launches torchrun, and captures the online loss itself. *Source: `docs/submissions.md:157-168`.* ## How it is scored The challenge re-executes `train(ctx)` under a forced random initialization on the locked FineWeb-Edu train split, captures the single-pass online (predict-then-train) loss itself, and computes the prequential bits-per-byte score with a held-out delta tie-breaker. Any value this submission reports and any manifest it writes are ignored; the challenge authors `prism_run_manifest.v2.json`. *Source: `examples/tiny-1m/README.md:22-27`.* ## Submit the bundle Submit the directory as a `.zip` bundle through the public route (when enabled) or let the BASE proxy forward it in production. ```http theme={"dark"} POST /v1/submissions Content-Type: application/json ``` ```json theme={"dark"} { "filename": "project.zip", "code": "", "metadata": {} } ``` *Source: `examples/tiny-1m/README.md:29-33`; `src/prism_challenge/routes.py:32-33`.* For the field-by-field contract, see [Submitting to PRISM](/challenges/prism/submit) and [Providing context](/challenges/prism/context). For the limits this example respects, see [Constraints](/challenges/prism/constraints). # How PRISM works Source: https://docs.joinbase.ai/challenges/prism/how-it-works The PRISM service architecture: a FastAPI app, SQLite state, a worker queue, the GPU evaluator, and the weights module. PRISM runs as a **FastAPI** application with **SQLite** state, internal BASE authentication, and GPU evaluation through the Docker broker. The challenge owns the data and the evaluation; the validator re-executes the miner's training loop under a forced random init and computes the score itself. *Source: `docs/architecture.md:1-7`.* ## High-level pipeline ```mermaid theme={"dark"} flowchart LR Miner[Miner] --> Proxy[BASE Proxy] Proxy --> Bridge[PRISM Bridge] Bridge --> DB[(SQLite)] Bridge --> Queue[Worker Queue] Queue --> Static[Static Sandbox + Param Cap + Distributed Contract] Static --> LLM[OpenRouter Hard Gate] LLM --> Broker[Docker Broker] Broker --> Reexec[Forced-Init Re-Execution Runner] Reexec --> Score[Prequential bpb + Held-out Delta] Score --> Weights[Dry-Run get_weights] ``` *Source: `docs/architecture.md:11-23`.* ## Main components | Component | Responsibility | Source | | ------------------ | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | | FastAPI app | Public and internal HTTP routes | `src/prism_challenge/app.py:24-88` | | Repository | SQLite persistence for submissions, scores, sources, eval jobs, and GPU leases | `src/prism_challenge/repository.py` | | Worker | Claims pending submissions, runs static + LLM gates, dispatches re-execution, finalizes scores | `src/prism_challenge/queue.py`; `src/prism_challenge/worker.py` | | Component resolver | Resolves the two-script contract and computes fingerprints | `src/prism_challenge/evaluator/components.py` | | Static sandbox | AST hard-blocks, forced-seed parameter-cap instantiation, multi-GPU static contract | `src/prism_challenge/evaluator/sandbox.py` | | LLM hard gate | OpenRouter review of both scripts; a `reject` is terminal before any GPU work | `src/prism_challenge/evaluator/llm_review.py` | | Container runner | Challenge-owned forced-init re-execution that captures the online loss stream | `src/prism_challenge/evaluator/container.py` | | Scoring | Prequential bits-per-byte plus held-out delta and anti-memorization gap | `src/prism_challenge/evaluator/scoring.py` | | Weights module | Converts normalized completed scores into dry-run weights | `src/prism_challenge/weights.py` | *Source: `docs/architecture.md:27-37`.* ## Subnet integration BASE is responsible for miner-facing upload security. It verifies signatures, timestamps, nonces, and hotkey identity before forwarding a submission to PRISM. PRISM receives verified submissions on the internal bridge route: ```text theme={"dark"} POST /internal/v1/bridge/submissions ``` This is an **internal** route. The bridge trusts only internal authentication and the verified hotkey header (`X-Platform-Verified-Hotkey`); miner-supplied identity headers are not trusted. *Source: `src/prism_challenge/app.py:68-75`; `docs/architecture.md:39-52`.* ## Execution model PRISM does not execute miner submissions directly in the master process. The worker performs static inspection and the LLM hard gate, then sends the project to an isolated evaluator container through the Docker broker: ```text theme={"dark"} PRISM worker -> DockerExecutor -> Docker broker -> GPU evaluator container ``` The pre-GPU static gates run in this order, and a rejection at any of them is terminal before the LLM review and before any GPU work: 1. AST sandbox hard-blocks over both scripts. 2. Forced-seed `build_model` instantiation and the 150M parameter cap. 3. The multi-GPU static contract and single-node bound. *Source: `docs/architecture.md:64-80`.* ## Forced-init re-execution (anti-cheat core) The challenge harness drives every scored run; the miner code only supplies the model and the loop body. 1. The harness writes a challenge-owned runner that imports the miner's `architecture.py` and `training.py`, sets the global seeds and deterministic flags **before** any miner code runs, then launches `torchrun --standalone --nnodes=1 --nproc-per-node=1`. 2. The runner installs an instrumented loss capture. The data iterator yields fresh, single-pass batches from the read-only locked `train` split in a challenge-controlled order, and the challenge records each per-batch loss **before** the optimizer updates on it. Because the data is single-pass, this online training loss is the prequential code-length by construction. 3. The challenge authors `prism_run_manifest.v2.json` from the captured stream. Any manifest the miner writes is discarded; any metric the miner reports is ignored. The eval container is non-root, runs with a read-only rootfs except `artifacts_dir`, uses `network=none`, and is bounded by a wall-clock budget that is only a safety cap, never part of the score. *Source: `docs/architecture.md:82-102`.* ## State model PRISM stores state in SQLite. Important tables include `miners`, `submissions`, `eval_jobs`, `gpu_leases`, `scores`, `submission_sources`, `llm_reviews`, `plagiarism_reviews`, and `epochs`. * `eval_jobs` tracks each evaluation attempt (including the `level='l1'` static-tracking placeholder created at submission time, which is not GPU work). * `gpu_leases` records the exclusive single-GPU lease for a scored run. * `scores` holds the challenge-computed prequential bits-per-byte `final_score` and its metrics payload. *Source: `docs/architecture.md:104-121`; `src/prism_challenge/db.py:100-159`.* ## Scoring flow After the forced-init re-execution completes with a valid challenge-authored `prism_run_manifest.v2.json`, scoring computes everything from the challenge-owned capture: * the prequential bits-per-byte primary score (lower bpb yields a better `final_score`); * the held-out delta-over-random-init tie-breaker on the secret `val` split; * the train-vs-held-out anti-memorization gap, which penalizes an excessive gap; * a step-0 / smuggled-weights anomaly multiplier that zeroes an anomalous run. The leaderboard orders by `final_score` with a deterministic earliest-commit-wins tie-break, and `get_weights` returns one normalized, dry-run weight per hotkey. Weights are never written on-chain. *Source: `docs/architecture.md:123-146`; `src/prism_challenge/repository.py:506`; `src/prism_challenge/weights.py:21-31`.* ## The weights module `get_weights` converts completed scores into normalized weights: it reads each epoch's score rows and keeps, per hotkey, the best `final_score`, then normalizes the survivors to sum to 1.0. Weights are always **dry-run**. Earlier versions split rewards across separate architecture and training pools. The live weights path normalizes the best `final_score` per hotkey and the v1-NAS architecture/training ownership pools are retired from the score. *Source: `src/prism_challenge/weights.py:21-31`; `docs/scoring.md:80-89`; `docs/architecture.md:162`.* ## Failure handling A submission ends in one of these states: `pending`, `running`, `completed`, `failed`, `rejected`, or `held`. * **Rejected** - failed static review, the two-script contract, the LLM hard gate, or duplicate review. * **Failed** - passed the gates but failed the re-execution, scoring, or infrastructure. * **Held** - quarantined by the LLM review pending operator attention. *Source: `docs/architecture.md:148-162`.* See [PRISM API](/challenges/prism/api) for the full route list and [Running PRISM](/challenges/prism/operators) for deployment. # PRISM challenge operators Source: https://docs.joinbase.ai/challenges/prism/operators Challenge-operator guide for PRISM (not BASE subnet validator mint): local validation, runtime config, data plane, compute budget, LLM gate. This guide is for **PRISM challenge operators** (running the PRISM service). It is not the BASE subnet validator pack under Validators. This guide covers local validation and production-oriented configuration for running PRISM as a BASE challenge. *Source: `docs/operators.md:1-5`.* ## Installation ```bash theme={"dark"} git clone https://github.com/BaseIntelligence/prism.git cd prism uv sync --frozen --extra dev ``` *Source: `docs/operators.md:6-12` (repo URL per `SOURCES.md:51`).* ## Local validation ```bash theme={"dark"} .venv/bin/ruff check src .venv/bin/mypy src/prism_challenge/evaluator .venv/bin/python -m pytest tests -q ``` *Source: `docs/operators.md:14-20`.* ## Required runtime configuration At minimum, PRISM needs a database URL, a shared-token file, and an execution backend: ```bash theme={"dark"} PRISM_DATABASE_URL=sqlite+aiosqlite:////data/prism.sqlite3 PRISM_SHARED_TOKEN_FILE=/run/secrets/platform/challenge_token PRISM_EXECUTION_BACKEND=platform_gpu ``` The shared token must match the token configured in the master for this challenge. *Source: `docs/operators.md:22-32`.* ## Docker broker configuration Production evaluation uses the Docker broker with the augmented evaluator image: ```bash theme={"dark"} PRISM_DOCKER_ENABLED=true PRISM_DOCKER_BACKEND=broker PRISM_DOCKER_BROKER_URL=http://platform-docker-broker:8082 PRISM_DOCKER_BROKER_TOKEN_FILE=/run/secrets/platform/challenge_token PRISM_PLATFORM_EVAL_IMAGE= PRISM_PLATFORM_EVAL_GPU_COUNT=1 PRISM_DOCKER_NETWORK=none ``` Set `PRISM_PLATFORM_EVAL_IMAGE` to the evaluator image published for your deployment. The image must ship `sentencepiece` and an offline tiktoken gpt2 cache so reference tokenizers load with no network. The scored run is single-node and uses `torchrun --standalone --nnodes=1 --nproc-per-node=1`. *Source: `docs/operators.md:34-50` (evaluator image literal omitted; it references a non-BASE registry - supply your own).* ## Locked FineWeb-Edu data plane The broker bind-mounts the locked FineWeb-Edu data read-only into the eval container, which runs with `network=none`: ```bash theme={"dark"} PRISM_PLATFORM_EVAL_DATA_DIR=/data/fineweb-edu/train # miner-visible, read-only PRISM_PLATFORM_EVAL_VAL_DATA_DIR=/data/fineweb-edu/val # secret; scorer-only, never mounted into eval PRISM_PLATFORM_EVAL_REFERENCE_TOKENIZER_DIR=/opt/reference-tokenizers ``` `HF_HUB_OFFLINE=1` and `HF_DATASETS_OFFLINE=1` are set inside the eval container. The `val`/`test` splits are secret and must never be exposed to a miner script. *Source: `docs/operators.md:52-64`.* ## Compute budget The score is compute-normalized; wall-clock is only a safety cap, enforced in layers: ```bash theme={"dark"} PRISM_PLATFORM_EVAL_BUDGET_SECONDS=1200 # graceful stop; score the partial stream PRISM_PLATFORM_EVAL_WATCHDOG_GRACE_SECONDS=120 # hard watchdog above the graceful budget PRISM_PLATFORM_EVAL_TIMEOUT_SECONDS=1800 # outer docker/broker backstop PRISM_PLATFORM_EVAL_ARTIFACTS_QUOTA_BYTES=2147483648 ``` *Source: `docs/operators.md:66-75`.* ## LLM hard gate configuration The OpenRouter LLM hard gate is enabled by default and reviews both scripts before any GPU work: ```bash theme={"dark"} PRISM_LLM_REVIEW_ENABLED=true PRISM_OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 PRISM_OPENROUTER_MODEL=openai/gpt-4o PRISM_OPENROUTER_API_KEY_FILE=/run/secrets/openrouter_api_key ``` A `reject` from the gate is terminal. The eval container carries no OpenRouter key (the gate runs host-side before the container is launched). *Source: `docs/operators.md:77-89`; `config.example.yaml:60-63`.* ## Multi-GPU static contract ```bash theme={"dark"} PRISM_DISTRIBUTED_CONTRACT_POLICY=reject # reject | flag | off PRISM_PLATFORM_EVAL_MAX_GPU_COUNT=8 ``` `reject` (the default) hard-rejects a non-distributed `training.py`; `flag` advances but logs; `off` skips the check. *Source: `docs/operators.md:91-99`.* ## Duplicate review ```bash theme={"dark"} PRISM_PLAGIARISM_ENABLED=true ``` An exact-source-hash duplicate is rejected, and a borderline-similarity quarantine is folded into a terminal rejection at ingress. There is no operator hold-resolution surface. *Source: `docs/operators.md:101-108`.* ## Running locally ```bash theme={"dark"} PRISM_SHARED_TOKEN=dev-secret \ PRISM_DATABASE_URL=sqlite+aiosqlite:///./prism.sqlite3 \ .venv/bin/uvicorn prism_challenge.app:app --host 0.0.0.0 --port 8000 ``` *Source: `docs/operators.md:110-116`.* ## Health checks ```bash theme={"dark"} curl http://localhost:8000/health curl http://localhost:8000/version ``` Internal weights require the shared token: ```bash theme={"dark"} curl -H "Authorization: Bearer dev-secret" \ -H "X-Platform-Challenge-Slug: prism" \ http://localhost:8000/internal/v1/get_weights ``` *Source: `docs/operators.md:124-137`.* ## Deployment In a deployment, PRISM registers as a challenge image reached by the master over the internal challenge network. Public miner traffic goes through the proxy, which verifies signatures and forwards to PRISM. Weights are exposed only via `get_weights` and are always dry-run. *Source: `docs/operators.md:118-122`.* ## Troubleshooting | Symptom | Likely cause | | ------------------------------------ | --------------------------------------------------------------------------------------------- | | `invalid internal token` | Shared token mismatch between the master and PRISM | | submission rejected before container | Static sandbox, two-script contract, param cap, distributed contract, or LLM hard-gate reject | | submission held | LLM review quarantine (transient error or ambiguous verdict) | | evaluation failed | Broker, image, GPU, timeout, missing locked data, or container error | | empty weights | No completed, scored submissions yet | | `missing_locked_data` | The read-only FineWeb-Edu train mount is absent or empty on the GPU node | *Source: `docs/operators.md:139-148`.* # PRISM overview Source: https://docs.joinbase.ai/challenges/prism/overview PRISM is the decentralized neural architecture search challenge that scores how fast a model learns from scratch on locked data. PRISM is a BASE challenge for **decentralized neural architecture search (NAS)**. [Miners](/concepts/glossary#miner) submit a model architecture and a training recipe, and the challenge competes them on a single, cheat-resistant question: **how fast does a model learn from scratch?** PRISM fixes the dataset and the evaluation protocol; it does not fix the model search space beyond a Python contract, a static sandbox, a parameter cap, and resource limits. PRISM challenge source - decentralized neural architecture search; miners submit architectures and training recipes for competitive evaluation. ## What PRISM measures PRISM does not ask miners to train a frontier model. It asks a sharper question: given a fixed dataset and a forced random initialization, how quickly does a model learn? PRISM measures that as **online compression** - the better a model predicts each new chunk of text *before* training on it, the better it compresses the stream, and the better it scores. PRISM is designed to answer questions such as: * Which architectures learn fastest from scratch under a fixed compute budget? * Which training loops (optimizer, schedule, data ordering, distributed strategy) improve sample efficiency? * Which ideas hold up when the validator - not the miner - controls the seed, the data, and the metric? *Source: `docs/overview.md:8-19` (clone path in [Sources](#sources)).* ## What miners submit A submission is a two-script bundle (a `.zip` archive or a directory snapshot): * `architecture.py` exposes `build_model(ctx)`, a factory returning a `torch.nn.Module`. * `training.py` exposes `train(ctx)`, the miner-owned training loop. An optional `prism.yaml` manifest declares the entrypoints, the chosen tokenizer, and the submit mode. A single combined module no longer satisfies the contract: the architecture and training roles must be two distinct scripts. *Source: `docs/overview.md:40-49`; `src/prism_challenge/evaluator/components.py:99-103`.* ## Why the miner owns the loop but not the score The miner owns the model and the training procedure, including multi-GPU scaling. The challenge owns everything that makes the comparison fair and cheat-resistant: * the dataset content and the secret `val`/`test` splits; * the forced random seed and deterministic flags; * the data order and the single-pass online-loss capture; * the scoring. Any metric the miner reports and any manifest the miner writes are ignored. Scoring always reads the challenge-authored `prism_run_manifest.v2.json`. *Source: `docs/overview.md:50-61`.* ## The signal that matters The primary signal is the **prequential bits-per-byte (bpb)**: the area under the from-scratch online loss curve, normalized by the raw UTF-8 bytes consumed. A model that learns faster compresses better and ranks higher. A held-out delta-over-random-init breaks near-ties, and an excessive train-vs-held-out gap flags memorization and penalizes the score. *Source: `docs/overview.md:76-79`; `docs/scoring.md:8-26`.* See [Scoring](/challenges/prism/scoring) for the math and [How PRISM works](/challenges/prism/how-it-works) for the full pipeline. ## Anti-cheat by construction PRISM is designed so common cheats are inert rather than merely detected: * **No pretrained weights** - the validator forces random init, so smuggled weights produce an anomalous step-0 loss that zeroes the score; the container runs `network=none`. * **No metric manipulation** - the challenge re-executes and computes the metric itself from the online loss it captured. * **No memorization** - the `val`/`test` splits are secret and never exposed to the miner; an excessive train-vs-held-out gap penalizes the score. * **Determinism** - fixed seeds and deterministic algorithms make the same submission reproduce the same score within tolerance. *Source: `README.md:125-139`.* ## Where to go next Build and submit your first two-script bundle. The FastAPI service, worker queue, GPU evaluator, and weights module. The three submit modes and the `prism.yaml` manifest. Prequential bits-per-byte, tie-breaks, and weights. ## Sources All citations on this page reference the `prism` repository pinned at SHA `6f3e1fb8a5ad5d8ed007334039a85a3168792c61` (see `SOURCES.md`), cloned at `/projects/baseintelligence/sources/prism`. # PRISM quickstart Source: https://docs.joinbase.ai/challenges/prism/quickstart Build a valid two-script PRISM bundle, declare it with prism.yaml, and submit it for forced-init re-execution scoring. This quickstart takes you from zero to a submitted PRISM bundle. PRISM scores how fast your model learns from scratch on locked FineWeb-Edu data, so the goal is a clean, deterministic two-script bundle that stays inside the sandbox and the parameter cap. ## Prerequisites Before you begin, you need: * A registered miner **hotkey** on BASE (netuid **100**). * Python with `torch`, able to build a `torch.nn.Module` under the 150M parameter cap. * A two-script project: a model `architecture.py` and a training `training.py`. *Source: `src/prism_challenge/evaluator/interface.py:26` (150M cap); `docs/miner/README.md:1-18`.* ## Get started Expose a pure `build_model(ctx)` factory that returns a `torch.nn.Module`. It must not read data, open files, touch the network, or reference the dataset. ```python theme={"dark"} import torch class TinyModel(torch.nn.Module): def __init__(self, vocab_size): super().__init__() self.embedding = torch.nn.Embedding(vocab_size, 8) self.linear = torch.nn.Linear(8, vocab_size) def forward(self, tokens): return self.linear(self.embedding(tokens)) def build_model(ctx): return TinyModel(ctx.vocab_size) ``` *Source: `docs/submissions.md:137-153`.* Expose `train(ctx)` - the loop you own. Build the optimizer and schedule, read the locked train split from `ctx.data_dir`, tokenize, run the loop, handle multi-GPU, and write only under `ctx.artifacts_dir`. ```python theme={"dark"} from architecture import build_model def train(ctx): model = build_model(ctx) # construct the optimizer/schedule, read ctx.data_dir, tokenize, run the loop, # handle multi-GPU, and write only under ctx.artifacts_dir. ... ``` *Source: `docs/submissions.md:157-165`.* Add a `prism.yaml` to declare the entrypoints and the tokenizer. When absent, PRISM uses the default entrypoints (`architecture.py`, `training.py`) and symbols (`build_model`, `train`). ```yaml theme={"dark"} architecture: entrypoint: architecture.py training: entrypoint: training.py tokenizer: gpt2 ``` *Source: `docs/submissions.md:43-49`; `examples/tiny-1m/prism.yaml:1-5`.* Package the scripts as a `.zip` (or directory snapshot). A minimal bundle: ```text theme={"dark"} project.zip architecture.py training.py prism.yaml # optional ``` *Source: `docs/submissions.md:130-135`.* Submit the bundle to the public route (when public submissions are enabled), or let the BASE proxy forward it in production. ```http theme={"dark"} POST /v1/submissions Content-Type: application/json ``` ```json theme={"dark"} { "filename": "project.zip", "code": "", "metadata": {} } ``` *Source: `src/prism_challenge/routes.py:32-33`; `docs/miner/README.md:104-118`.* Poll the submission status and the leaderboard. `final_score` is the challenge-computed prequential bits-per-byte score (a lower bpb yields a higher `final_score`). ```http theme={"dark"} GET /v1/submissions/{submission_id} GET /v1/leaderboard ``` *Source: `src/prism_challenge/routes.py:70-103`.* ## What happens after you submit 1. PRISM validates the two-script contract and runs the static AST sandbox. 2. An OpenRouter LLM hard gate reviews both scripts and can reject before any GPU work. 3. The validator re-executes your `training.py` under a forced random init on the locked train split. 4. The challenge computes the prequential bits-per-byte score plus the held-out delta tie-breaker. 5. Scores rank on the leaderboard and convert into normalized, dry-run weights. *Source: `README.md:34-42`.* Make your loop deterministic under the forced seed and correct at `world_size=1`. The scored run uses one physical GPU (`nproc=1`), so a loop that only works multi-GPU will not score. See [Constraints](/challenges/prism/constraints). ## Next steps The three submit modes and full `prism.yaml` reference. The `PrismContext` fields your scripts receive. A complete tiny two-script bundle. How your bundle is scored and ranked. # Scoring Source: https://docs.joinbase.ai/challenges/prism/scoring How PRISM scores a submission: prequential bits-per-byte, the held-out delta tie-breaker, the anti-memorization gap, anomaly zeroing, and weights. PRISM scores a single thing: a model's ability to learn from scratch, measured as online compression. The primary metric is a **prequential bits-per-byte (bpb)** score that the challenge computes itself from a forced-init re-execution. A held-out delta-over-random-init breaks near-ties, and an anti-memorization gap penalizes overfitting. **Lower bits-per-byte is better.** *Source: `docs/scoring.md:1-6`.* ## Primary metric: prequential bits-per-byte During the forced-init re-execution, the challenge feeds the model fresh, single-pass batches from the locked train split and records the model's loss on each new batch **before** the optimizer updates on it. Because the data is single-pass, this online (predict-then-train) loss is the prequential code-length by construction. The challenge integrates that code-length over the whole run and normalizes it by the raw UTF-8 bytes of text covered: ```text theme={"dark"} bpb = (sum over consumed tokens of -log2 p(token)) / total_bytes_covered ``` Because the denominator is bytes, the metric is **tokenizer-agnostic**. Because it integrates the whole loss curve, a single good checkpoint cannot game it. Because each token is scored before being trained on, there is no held-out leakage by construction. And because the validator forces random init, smuggled pretrained weights are inert. *Source: `docs/scoring.md:8-26`.* ## From bpb to `final_score` `final_score` is a documented monotone-decreasing transform of bpb, so a **lower** bpb yields a **better** (higher) `final_score`: ```text theme={"dark"} final_score = 1 / (1 + bpb) # before tie-break, penalty, and anti-cheat multiplier ``` This transform is implemented in source as `bpb_to_final_score`, which returns `1.0 / (1.0 + max(0.0, float(bpb)))`. The leaderboard's `ORDER BY final_score DESC` therefore ranks better learners first. *Source: `docs/scoring.md:32-34`; `src/prism_challenge/evaluator/scoring.py:151-153`.* ## Compute normalization, not wall-clock The score is **compute-normalized**: it is reported and normalized by tokens consumed (and, optionally, estimated FLOPs), never by wall-clock time. A faster GPU or more GPUs cannot buy a better score; wall-clock is only a safety cap on the run. This keeps scores fair across the 1-to-8 GPU range even though the scored run uses one physical GPU. *Source: `docs/scoring.md:36-41`; `docs/scaling.md:48-67`.* ## Tie-breaker: held-out delta over random init When two submissions are near-equal on bpb, the challenge breaks the tie with the held-out delta on the secret `val` split: ```text theme={"dark"} heldout_delta = bpb(random-init twin on val) - bpb(trained model on val) ``` A larger improvement over the random-init twin is better. The held-out delta is folded into `final_score` as a **bounded** tie-break term: it can only reorder submissions whose bpb is within a small epsilon of each other, so a strictly lower bpb is never ranked worse on the primary axis. When no secret val split is scored for a run, the run is graded on bpb alone with no tie-break. *Source: `docs/scoring.md:43-56`; `src/prism_challenge/evaluator/scoring.py:24-31`.* ## Anti-memorization gap (stability) The challenge measures the train-vs-held-out gap (the converged train bpb against the held-out val bpb on the same byte basis). An excessive gap flags memorization and multiplies a penalty into `final_score`, so a memorizer ranks below an equivalent non-memorizing learner. The gap comparison is basis-consistent so a benign learner is not falsely flagged. *Source: `docs/scoring.md:58-63`.* ## Anomaly zeroing A step-0 / smuggled-weights anomaly (an impossibly low initial loss under forced random init) drives the anti-cheat multiplier to zero, so an anomalously good bpb is flagged and zeroed rather than rewarded. A degenerate run (zero coverage, non-finite, or out-of-band bpb) is failed rather than scored. *Source: `docs/scoring.md:66-70`.* ## Scaling signals PRISM keeps the score compute-normalized so hardware never changes the ranking, and it records a typed, observability-only compute block in the manifest - the GPUs leased (`gpu_count`, which is 1 for the scored `nproc=1` path), the launch shape (`world_size`, `nproc_per_node`, `device`), and the realized parameter count. The `final_score` never reads `gpu_count`, so there is **no GPU-count reward and no multi-GPU scaling bonus**. Two official scored execution modes run on the locked FineWeb-Edu data: | Mode | Purpose | Dataset target | | ----------------- | ------------------------------------ | ---------------------------------------------------- | | `gpu_proxy_eval` | Default official scored re-execution | FineWeb-Edu `sample-10BT` locked shards | | `full_scale_eval` | Larger official scored re-execution | FineWeb-Edu `sample-10BT` then `sample-100BT` phases | *Source: `docs/scaling.md:9-19`, `:61-67`.* ## Leaderboard and tie-break ordering The leaderboard ranks by `final_score` (so by bpb and the folded-in held-out delta). When two submissions are still equal, the final deterministic tie-break is **earliest-commit-wins**, then submission id - implemented as `ORDER BY sc.final_score DESC, s.created_at ASC, s.id ASC`. Each hotkey appears at most once: the best submission per hotkey survives. *Source: `docs/scoring.md:72-77`; `src/prism_challenge/repository.py:506`.* ## Weights `get_weights` converts completed scores into normalized weights: one weight per hotkey, taken from that hotkey's best `final_score`, normalized to sum to 1.0. Weights are always **dry-run** and are never written on-chain. The legacy raw-loss term and the v1-NAS architecture/training ownership pools are retired from the score. Every number above is recomputed by the challenge from the challenge-authored `prism_run_manifest.v2.json`; miner-reported metrics and miner-written manifests are ignored. *Source: `docs/scoring.md:80-89`; `src/prism_challenge/weights.py:21-31`.* ## Reference studies PRISM's scoring cites the following studies (reproduced from the source scoring doc): | Area | Study | PRISM implication | | --------------------------- | --------------------- | ------------------------------------------------------------------------- | | Prequential / online coding | Dawid, 1984 | Score the integrated online loss, not a final checkpoint. | | Minimum description length | Rissanen, 1978 | Treat compression (code-length) as the learning signal. | | Scaling laws | Kaplan et al., 2020 | Compare loss trajectories under matched compute. | | Compute-optimal scaling | Hoffmann et al., 2022 | Normalize by tokens/compute so over/under-training does not skew ranking. | | Dataset provenance | Penedo et al., 2024 | Freeze the data revision and shards for reproducible runs. | *Source: `docs/scoring.md:91-99`.* # Submitting to PRISM Source: https://docs.joinbase.ai/challenges/prism/submit The two-script contract, the prism.yaml manifest, and the three submit modes: full, architecture_only, and training_for_arch. A PRISM submission is a **two-script** bundle: a `.zip` archive (or a directory snapshot) containing a model `architecture.py` and a training `training.py`. The miner owns the model and the training loop; the challenge owns the dataset and the scoring. A single combined module no longer satisfies the contract. *Source: `docs/submissions.md:1-10`; `src/prism_challenge/evaluator/components.py:99-103`.* ## The two-script contract A bundle must contain two **distinct** scripts. `architecture.py` exposes a model factory: ```python theme={"dark"} def build_model(ctx): return MyModel(ctx.vocab_size) ``` `build_model(ctx)` must return a `torch.nn.Module`. It must not read data, open files, touch the network, or reference the dataset. `training.py` exposes the miner-owned training loop: ```python theme={"dark"} def train(ctx): model = ctx.build_model() # build the optimizer/schedule, read the locked train split from ctx.data_dir, # tokenize, run the loop, handle multi-GPU, write only under ctx.artifacts_dir. ... ``` The default entrypoints are `architecture.py` and `training.py`, and the default symbols are `build_model` and `train`. *Source: `docs/submissions.md:12-54`; `src/prism_challenge/evaluator/interface.py:11-14`.* ## The `prism.yaml` manifest An optional `prism.yaml` (or `prism.yml`) declares the entrypoints, the tokenizer, and the submit mode. When the manifest is **absent**, PRISM uses the default entrypoints and symbols. When it is **present**, declared entrypoints are honored exactly, with **no silent fallback**. *Source: `src/prism_challenge/evaluator/components.py:20`, `:54-105`.* ### Manifest fields | Field | Meaning | Source | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- | | `kind` | Submit mode: `full`, `architecture_only`, or `training_for_arch` (default `full`) | `src/prism_challenge/evaluator/components.py:21-22`, `:76-78` | | `architecture_id` | Optional id linking the submission to an existing architecture | `src/prism_challenge/evaluator/components.py:79-81` | | `architecture.entrypoint` | Path to the architecture script (default `architecture.py`) | `src/prism_challenge/evaluator/components.py:85-87` | | `training.entrypoint` | Path to the training script (default `training.py`) | `src/prism_challenge/evaluator/components.py:88-90` | | `.factory` / `.function` / `.entry` | Override the role's symbol name (the `entrypoint` may also use an `entrypoint.py::symbol` suffix) | `src/prism_challenge/evaluator/components.py:131-141` | | `.files` | Extra files (string or list) to include in the role's fingerprint | `src/prism_challenge/evaluator/components.py:242-259` | | `tokenizer` | The chosen tokenizer, e.g. `gpt2` | `docs/submissions.md:43-49`; `examples/tiny-1m/prism.yaml:5` | A minimal manifest: ```yaml theme={"dark"} architecture: entrypoint: architecture.py training: entrypoint: training.py tokenizer: gpt2 ``` *Source: `examples/tiny-1m/prism.yaml:1-5`.* ## Submit modes The `kind` field selects one of three submit modes. The set of valid kinds is fixed in source, and an unsupported kind is rejected with a contract error. *Source: `src/prism_challenge/evaluator/components.py:21-22`, `:76-78`.* The default mode (`kind: full`, also used when `prism.yaml` is absent). The bundle carries **both** roles - a complete architecture and its training recipe - and both are fingerprinted from the bundle. ```yaml theme={"dark"} kind: full architecture: entrypoint: architecture.py training: entrypoint: training.py tokenizer: gpt2 ``` *Source: `src/prism_challenge/evaluator/components.py:22`, `:67-90`.* Declares the submission as an architecture-focused entry (`kind: architecture_only`). The `kind` is recorded on the resolved components and carried into the semantic signature as `project_kind`. ```yaml theme={"dark"} kind: architecture_only architecture: entrypoint: architecture.py training: entrypoint: training.py tokenizer: gpt2 ``` The two-script contract still applies: a distinct `architecture.py` (`build_model`) and `training.py` (`train`) are required, because the harness must re-execute a real loop to produce a score. *Source: `src/prism_challenge/evaluator/components.py:21`, `:99-105`; `src/prism_challenge/evaluator/component_signatures.py:61`.* Declares a training recipe targeting an existing architecture (`kind: training_for_arch`). Pair it with `architecture_id` to attribute the recipe to that architecture. ```yaml theme={"dark"} kind: training_for_arch architecture_id: "" architecture: entrypoint: architecture.py training: entrypoint: training.py tokenizer: gpt2 ``` The `architecture_id` is read from the manifest and recorded on the resolved components; the bundle must still supply two distinct scripts so the loop can be re-executed. *Source: `src/prism_challenge/evaluator/components.py:21`, `:79-81`, `:99-105`.* ## Architecture-vs-training ownership and attribution The submit mode declares **intent** about which role a submission contributes - an architecture, a training recipe for an existing architecture, or both. PRISM records this as the `kind` plus the optional `architecture_id`, and the SQLite schema keeps `architecture_families` and `training_variants` tables that the family- and variant-listing routes expose. However, the **live score and weights do not split rewards by role**. `get_weights` keeps, per hotkey, the best `final_score` and normalizes the survivors; the v1-NAS architecture/training ownership reward pools are retired from the score, and the component-attribution holds and ownership-event machinery have been decommissioned. The `GET /v1/architectures` and `GET /v1/training-variants` routes are retained as **legacy** endpoints for API compatibility. They do not drive the live weighting. *Source: `src/prism_challenge/evaluator/components.py:21-22`, `:79-81`; `src/prism_challenge/db.py:100-131`; `src/prism_challenge/weights.py:21-31`; `docs/scoring.md:89`; `docs/architecture.md:162`.* ## The challenge owns the data and the score PRISM re-executes the miner's `training.py` under a forced random init and a fixed seed, and records the online loss stream itself. Any value the miner reports is ignored, and the challenge authors the run manifest. See [How PRISM works](/challenges/prism/how-it-works) for the forced-init re-execution flow and [Scoring](/challenges/prism/scoring) for the bits-per-byte math. *Source: `docs/submissions.md:76-81`.* ## ZIP safety rules ZIP submissions are extracted defensively: no path traversal, no symlinks, a limited file count, limited total bytes, and only approved text or code suffixes. Unsupported or unsafe archives are rejected before evaluation. *Source: `docs/submissions.md:173-183`.* # Challenge SDK Source: https://docs.joinbase.ai/challenges/sdk The shared challenge-side SDK that generated challenges use for evaluation, config, and weights. The challenge SDK is the shared challenge-side toolkit that generated challenges import. It lives in the `platform_network.challenge_sdk` package and centers on running evaluation containers securely and reading challenge configuration from the environment. ## What the SDK exports The package exposes a secure Docker executor and its supporting types: ```python theme={"dark"} from platform_network.challenge_sdk import ( DockerExecutor, DockerExecutorSettings, DockerExecutorError, DockerLimits, DockerMount, DockerRunResult, DockerRunSpec, ) ``` * `DockerExecutor` is a secure Docker CLI executor for challenge-side evaluation containers. * `DockerRunSpec`, `DockerMount`, and `DockerLimits` describe a run: the image, mounts, and resource limits. * `DockerRunResult` captures the outcome, and `DockerExecutorError` is raised when a container cannot be executed safely. ## Configuration `DockerExecutorSettings` reads environment-backed settings with the `CHALLENGE_` prefix, so a challenge configures its executor without code changes: | Setting | Env var | Default | | ---------------- | ----------------------------- | ------- | | Docker enabled | `CHALLENGE_DOCKER_ENABLED` | `false` | | Network mode | `CHALLENGE_DOCKER_NETWORK` | `none` | | CPUs | `CHALLENGE_DOCKER_CPUS` | `2.0` | | Memory | `CHALLENGE_DOCKER_MEMORY` | `4g` | | PIDs limit | `CHALLENGE_DOCKER_PIDS_LIMIT` | `512` | | Read-only rootfs | `CHALLENGE_DOCKER_READ_ONLY` | `true` | | Backend | `CHALLENGE_DOCKER_BACKEND` | `cli` | The defaults are deliberately locked down: evaluation containers run with no network and a read-only root filesystem unless a challenge opts out. ## Cross-node mount transport A broker eval job may be scheduled on a different Swarm node than the broker process. GPU jobs land on the GPU worker while the broker runs on the manager, so a host bind-mount materialized on the broker node does not exist on the worker. The SDK's mount transport moves mount content across nodes without any shared filesystem, using only channels Swarm already distributes (environment and argv) and collects (`docker service logs`). Inbound mount content rides in chunked, base64 gzip-tar environment variables and is extracted into a node-local tmpfs by a bootstrap wrapper; writable mounts are tarred back to stdout between unique sentinels and decoded by the executor. Non-regular tar members such as symlinks are dropped so a mount archive cannot resolve to an attacker-chosen path. ## How it relates to a challenge Generated challenges combine the SDK executor with the weight contract and database helpers described in [Creating a challenge](/challenges/creating). For a worked example of an SDK-driven challenge that runs evaluation containers through the broker, the Agent Challenge worker uses this execution substrate; see the [Agent Challenge overview](/challenges/agent-challenge). ## Related The Docker broker that dispatches SDK evaluation jobs. How eval jobs are scheduled across CPU and GPU nodes. *** Sources: `base/src/platform_network/challenge_sdk/__init__.py:1` (exports), `base/src/platform_network/challenge_sdk/config.py:8` (`DockerExecutorSettings`, `CHALLENGE_` prefix and defaults), `base/src/platform_network/challenge_sdk/executors/docker.py:1` (secure Docker CLI executor), `base/src/platform_network/challenge_sdk/mount_transport.py:1` (cross-node mount transport), `agent-challenge/README.md:262` (own\_runner backend uses the broker execution substrate). # Challenges explained Source: https://docs.joinbase.ai/concepts/challenges A challenge is an independent competition plugged into BASE. Miners follow challenge docs; the subnet only coordinates weights. A **[challenge](/concepts/glossary#challenge)** is where work happens on BASE. The subnet is the coordination layer. Each challenge defines a competition with its own rules. ## What a challenge is A challenge is an independent repository and Docker image. It owns logic, public routes, submissions, scoring data, database schema, and challenge-local files. State lives on the challenge's own data volume, separate from the subnet control plane. Standard internal contract: ```text theme={"dark"} GET /health GET /version GET /internal/v1/get_weights ``` `get_weights` is how BASE reads raw hotkey weights. It is token-authenticated and not exposed on the public proxy. ## Primary challenges Software-engineering agents. Production scoring is miner self-deploy on Phala Intel TDX with attested review, RA-TLS golden key release, and Terminal-Bench eval from a baked guest cache. No Base LLM gateway on the scored path. [Agent Challenge miner pack →](/challenges/agent-challenge) Learn-from-scratch neural architecture search. Miners submit two scripts; the challenge re-executes training and computes prequential bits-per-byte. [PRISM miner pack →](/challenges/prism/overview) ## Additional challenges Owner-reviewed project bounties that need human judgment. [Guide →](/challenges/bounty-challenge) Dataset-generation harnesses for agentic coding conversations. [Guide →](/challenges/data-fabrication) Real repositories as fail-to-pass agent benchmarks. [Guide →](/challenges/agent-swe) ## How a challenge plugs in BASE routes public requests by slug, reads protected `get_weights`, and normalizes scores. Everything else is challenge-owned. Full catalog and miner packs. End-to-end flow. # Glossary Source: https://docs.joinbase.ai/concepts/glossary Definitions of the core BASE and Bittensor terms used throughout these docs. The terms you will meet across the BASE docs, defined once. Where a term has a fixed value or behavior in the canonical subnet, the value is noted. ### Subnet A subnet is a single network instance on Bittensor. BASE is one subnet that runs multiple [challenges](#challenge) under one [validator](#validator) network. See [what is BASE?](/concepts/overview). ### netuid The netuid is the subnet's id on the Bittensor chain. For BASE it is **100**. You set this value wherever you point a wallet, submitter, or validator at the subnet. ### Challenge A challenge is an independent repository and Docker image that defines a specific competition: its own submissions, scoring logic, database, and public routes. Challenges expose a standard internal weight contract to the subnet. See [challenges explained](/concepts/challenges). ### Miner A miner is a participant that produces work. A miner picks a [challenge](#challenge), follows that challenge's submission contract, and earns when the challenge exports a raw [weight](#weights) for the miner's [hotkey](#hotkey). See [miners and validators](/concepts/miners-and-validators). ### Validator A validator is a participant that publishes the final scores on-chain. On BASE a normal validator runs the submit-only on-chain submitter: it reads the master's final normalized vector and submits it to Bittensor. It runs no challenge orchestration. See [running a validator](/validators/overview). ### Weights Weights are the scores assigned to miners. Each challenge computes raw per-[hotkey](#hotkey) weights; the subnet normalizes them, applies [emissions](#emissions), and maps them to [UIDs](#uid) to form the vector submitted on-chain. See [weights and emissions](/concepts/weights-and-emissions). ### Emissions Emissions describe how influence is split across challenges, and ultimately how tokens are distributed to participants. Each active challenge carries an emission share; the subnet normalizes those shares across active challenges before combining scores. See [the incentive mechanism](/concepts/incentive-mechanism). ### UID A UID is a neuron's unique slot id within the subnet. The subnet maps miner [hotkeys](#hotkey) to UIDs when building the final weight vector. A hotkey with no known UID, and UID `0`, are skipped during aggregation. ### Hotkey A hotkey is the operational signing key. Miners sign submissions with their hotkey, and the validator submitter needs exactly one secret: the validator hotkey. In the canonical settings the wallet hotkey defaults to `default`. ### Coldkey A coldkey is the custody and ownership key. It holds funds and ownership and stays off operational nodes. The validator submitter guidance is explicit: never place coldkey material on the node, in logs, or in evidence files; copy only the [hotkey](#hotkey). See [wallets and registration](/concepts/wallets-and-registration). ### Epoch An epoch is the recurring interval on which the subnet recomputes and republishes weights. The master runs a weight epoch on a fixed interval (`epoch_interval_seconds`, default `360`), and the validator submitter polls and submits on its own interval. The on-chain submitter publishes the final vector at these boundaries. See the [weights pipeline](/architecture/weights-pipeline). ### Phala TDX Phala Intel TDX is the production CVM substrate for Agent Challenge attestation. Miners deploy measured CPU TDX guests for review and eval. Validators pin measurements and verify TDX quotes. See [Agent Challenge](/challenges/agent-challenge). ### compose\_hash `compose_hash` is the hash of a measured app compose definition used in the Agent Challenge measurement allowlist (with `mrtd`, RTMRs, and `os_image_hash`). A single-field mismatch fails allowlist checks. ### RA-TLS RA-TLS is remote-attestation TLS: client certificates carry dstack attestation extensions so the Agent Challenge key-release listener can bind mTLS peers to measured guests. Production golden key release uses raw TLS 1.3 (default `:8701`), not HTTP. ### Golden key The golden key is the validator-held decryption material for golden test assets on Agent Challenge. It is released only after RA-TLS quote verification and allowlist match for a specific eval run, and only into the measured eval guest that passed that grant. ### verified\_allow `verified_allow` (publicly reflected as a review-allowed phase) is the validator-verified review verdict that unlocks Agent Challenge eval prepare. Reject, escalate, expiry, and attestation failure never unlock benchmark work or scores. ## Keep reading The concepts these terms describe, in context. See the terms used in the end-to-end flow. # How the subnet works Source: https://docs.joinbase.ai/concepts/how-it-works From a challenge submission through scoring to on-chain weights on BASE. This page traces one unit of work through BASE. Read [What is BASE?](/concepts/overview) first if you need the role split. ## End-to-end flow Each [challenge](/concepts/glossary#challenge) has a slug such as `agent-challenge` or `prism`. The miner builds the artifact **that challenge** requires and submits through `/challenges/{slug}/...` (or the challenge-documented bridge path). Evaluation is challenge-owned. Example: Agent Challenge requires miner Phala TDX self-deploy with attested scores. PRISM re-executes training and computes its own metric. The subnet does not define a shared scoring rubric. At each [epoch](/concepts/glossary#epoch), the master reads every active challenge's raw hotkey weights through the protected weight contract. The aggregator normalizes each challenge's weights, applies configured emission shares, and maps hotkeys to Bittensor [UIDs](/concepts/glossary#uid). The on-chain submitter fetches the master's final normalized vector and submits it for the configured netuid. ## Components BASE typically runs as Docker Swarm with: * **Manager node:** public proxy API, broker, supervisor, long-lived challenge services, registry and emission config, final weight computation * **Worker nodes:** short-lived evaluation jobs when a challenge uses the broker model (not every challenge uses this for production scoring) The single public API serves registry read, latest weights, health, and `/challenges/*` passthrough. ## Isolation Each challenge runs as its own service with its own image, database volume, and public routes behind the proxy. Internal challenge routes stay off the public surface. ## Next What challenges are and which packs exist. How raw scores become one vector. # Incentive mechanism Source: https://docs.joinbase.ai/concepts/incentive-mechanism How BASE ties rewards to verifiable work through per-challenge scoring and normalized emissions. The incentive mechanism is the set of rules that decide who earns and how much. On BASE, rewards follow verifiable work: a [challenge](/concepts/glossary#challenge) scores what a [miner](/concepts/glossary#miner) actually produced, and the subnet converts those scores into on-chain [weights](/concepts/glossary#weights). ## The core idea Challenge scores are never submitted directly to Bittensor. Every score passes through the subnet, which applies a per-challenge [emission](/concepts/glossary#emissions) share and normalizes across all active challenges before anything reaches the chain. The flow is: 1. The challenge evaluates miner work. 2. The challenge exports raw [hotkey](/concepts/glossary#hotkey) weights. 3. The subnet applies that challenge's emission share. 4. The subnet normalizes across active challenge outputs. 5. The subnet maps hotkeys to Bittensor [UIDs](/concepts/glossary#uid). 6. [Validators](/concepts/glossary#validator) submit the final weights on-chain. ## Two levers decide your reward Each challenge owns its scoring rubric. A higher relative score raises your share of that challenge's weight. Each challenge carries a configured emission share. The same relative score is worth more in a larger-share challenge. The aggregator multiplies these together for every hotkey, sums per UID, and normalizes the result. See [weights and emissions](/concepts/weights-and-emissions) for the exact computation. ## Why it is built this way * **Isolation.** Each challenge owns its scoring, so a flaw in one challenge does not corrupt another's rewards. If a challenge fails, the subnet can isolate its contribution. * **Verifiability.** Challenges are designed so the work is evaluated, not self-reported. PRISM, for example, re-executes a miner's training loop and computes the score itself rather than trusting miner-reported numbers. * **One vector.** Many separate scores collapse into a single normalized weight vector, which is what Bittensor expects per [subnet](/concepts/glossary#subnet). Practical advice for miners: pick a challenge whose scoring you can move, and aim for a strong **relative** score. Absolute numbers across challenges are not comparable because each rubric and emission share differs. ## Where weights go The on-chain submitter reads the master's final vector and submits it to Bittensor for [netuid](/concepts/glossary#netuid) 100 at each [epoch](/concepts/glossary#epoch). Rewards then follow Bittensor's own emission rules for the subnet. ## Next The math behind the final vector. Pick where to compete. # Installation Source: https://docs.joinbase.ai/concepts/installation The high-level install paths for BASE, from a local checkout to a validator submitter to a full subnet bring-up. There is no single installer for everyone. What you install depends on what you want to do. This page maps the high-level paths; each role guide has the step-by-step detail. Not sure which path is yours? If you only want to publish weights on-chain, install the [submitter](#validator-the-submitter). If you are standing up the whole subnet, use the [full subnet](#operator-the-full-subnet) path. If you are developing or just exploring, start with the [local checkout](#developer-local-checkout). ## Choose your path Run the CLI and tests against the canonical repository. Install the submit-only on-chain weight submitter. Bring up the manager and workers on Docker Swarm. Set up keys before any install that touches the chain. ## Developer: local checkout For local development against the canonical `base` repository, install dependencies with [`uv`](https://docs.astral.sh/uv/) from the repository root: ```bash theme={"dark"} uv sync --extra dev --extra master ``` The repository documents the full local check (lint, format, types, and tests) alongside this command. From here you can run the [CLI](/quickstart). ## Validator: the submitter A normal [validator](/concepts/glossary#validator) installs only the submit-only on-chain submitter, a single systemd-managed process. It reads the master's latest weights and submits them on-chain; it runs no challenge orchestration and needs no database. The submitter ships three files that you copy into place, then enable as a systemd unit: The submitter process, its credential-free config, and the systemd unit are copied to their destinations on the validator node. ```bash theme={"dark"} systemctl daemon-reload systemctl enable --now platform-submitter.service ``` The config carries the [netuid](/concepts/glossary#netuid) (100), the wallet identity, and the master weights URL. Full detail is in the [validator path](/validators/installation). ## Operator: the full subnet Standing up the whole subnet is a Docker Swarm bring-up: a manager node for the control plane and challenge services, plus CPU/GPU worker nodes for short-lived evaluation jobs. The canonical entry point is the Swarm installer. The installer is **dry-run by default** and changes nothing until you pass `--apply`. Every destructive step sits behind its own explicit flag. ```bash theme={"dark"} ./deploy/swarm/install-swarm.sh # dry-run: prints the planned docker commands, changes nothing ./deploy/swarm/install-swarm.sh --apply # apply on a host you own ``` The full end-to-end bring-up (image builds, volume provisioning, worker enrollment, and the on-chain submitter) is documented in the repository deploy guide and the [architecture path](/architecture/overview). ## Next The keys you need before participating. Run something now. # Miners and validators Source: https://docs.joinbase.ai/concepts/miners-and-validators BASE validators operate the subnet; miners compete inside challenges. BASE has two participant roles that people often confuse: **validators** keep the subnet vector on-chain; **miners** produce challenge work. ## Miners A miner competes in a [challenge](/concepts/glossary#challenge). You do not build against a Base-global scoring game. 1. Choose a challenge 2. Read **that** challenge's miner documentation 3. Build the required artifact 4. Submit through the challenge's public routes 5. Complete any challenge-owned evaluation steps (example: Agent Challenge Phala self-deploy) 6. Track challenge status and leaderboards 7. Earn when the challenge exports a raw [weight](/concepts/glossary#weights) for your [hotkey](/concepts/glossary#hotkey) and BASE normalizes it Start at the [Miner hub](/miners/overview) or go straight to [Challenges](/challenges/overview). ## Validators A BASE validator operates subnet infrastructure and submits weights. Challenge TEE allowlists and PRISM GPU tables are **challenge operator** concerns, documented under each challenge, not under "become a BASE miner." Typical validator path: * Fetch the master's final normalized vector * Submit on-chain with the validator hotkey * Reconcile challenge registry when running the full `platform validator` stack Full path: [Validators](/validators/overview). Light validation: if infrastructure is too heavy, Bittensor CHK / stake-weight flows may let you back a recommended validator hotkey instead of running the stack. ## Who runs challenge services? Neither ordinary miners nor silent weight-only validators replace challenge service ownership. Challenge images run on the subnet manager as Swarm services. Some challenges (Agent Challenge production) additionally require **miners** to fund their own Phala CVMs for scored work. ## Next * [Wallets and registration](/concepts/wallets-and-registration) * [Weights and emissions](/concepts/weights-and-emissions) * [Miner hub](/miners/overview) * [Validator guide](/validators/overview) # What is BASE? Source: https://docs.joinbase.ai/concepts/overview BASE is the multi-challenge Bittensor subnet on netuid 100: validator infrastructure plus pluggable challenges. BASE is a **multi-challenge [Bittensor](https://bittensor.com/) [subnet](/concepts/glossary#subnet)**. It lets independent challenges run under one validator network, routes miner traffic to the right challenge, collects raw weights, normalizes [emissions](/concepts/glossary#emissions), maps miner [hotkeys](/concepts/glossary#hotkey) to [UIDs](/concepts/glossary#uid), and publishes the final vector for [validators](/concepts/glossary#validator) to submit on-chain. ## Role split (product truth) | Role | What they do | | ----------------------- | ----------------------------------------------------------------------------------------------------------------- | | **BASE validators** | Run subnet infrastructure: master, proxy, weight setting, submitter, wallet as validator | | **Miners** | Compete **inside challenges**; each challenge owns submit path, eval, costs, TEE | | **Challenge operators** | Run or configure a challenge service (allowlists, datasets, key release). Not the same as the BASE validator mint | | **Challenge authors** | Ship new challenge repos using the create/SDK guides | Miners do not "mine Base generic." Validators do not complete Agent Challenge TEE self-deploy for miners as the production score path. ## The subnet, in one picture * **Challenges** define the work. Each lives in its own repository and owns submissions, scoring, and miner experience. * **Miners** pick a challenge and follow that challenge. * **Validators** keep the network weight vector honest on-chain. ## netuid 100 BASE runs on **netuid 100**. Configure wallets, submitters, and validators against that netuid. ## What the subnet does * Tracks active challenges and emission shares * Routes `/challenges/{slug}/...` * Isolates challenge failure from the rest of the network * Normalizes challenge outputs, maps hotkeys to UIDs * Supports on-chain submit of the final vector ## Where to go next Submission to on-chain weights. Role boundaries without mixed messaging. Miner home and challenge catalog. BASE operator path. # Wallets and registration Source: https://docs.joinbase.ai/concepts/wallets-and-registration The keys you need to participate on BASE, how hotkeys and coldkeys differ, and where registration fits. Before you participate on BASE for real, you need a Bittensor wallet and a slot on the subnet. This page explains the two keys, the registration step, and the safety rules that matter most. ## Two keys, two jobs A Bittensor wallet has two keys with very different roles: * **[Coldkey](/concepts/glossary#coldkey)** - your custody and ownership key. It holds funds and ownership. Keep it offline and off operational nodes. * **[Hotkey](/concepts/glossary#hotkey)** - your operational signing key. Miners sign submissions with it; validators submit weights with it. The single most important safety rule: never place coldkey material on an operational node, in shell history, in logs, in screenshots, or in evidence files. Generate keys on a trusted machine and copy only the hotkey to the node that needs it. ## Generating wallets Wallet creation and on-chain registration use Bittensor's own tooling. See the [Bittensor documentation](https://docs.bittensor.com/) for creating a coldkey and hotkey and for registering on a subnet. Registration places your [hotkey](/concepts/glossary#hotkey) into a [UID](/concepts/glossary#uid) slot on the subnet. When you register, the subnet is **[netuid](/concepts/glossary#netuid) 100**. ## Pointing your wallet at the subnet The canonical configuration identifies the wallet with a name, a hotkey, and an optional wallet path. The defaults are: ```yaml theme={"dark"} network: netuid: 100 wallet_name: default wallet_hotkey: default wallet_path: null ``` A validator submitter sets these to its own identity (for example a dedicated wallet name and a `validator` hotkey) and reads the validator hotkey from the node's wallet path. ## Where the validator hotkey lives The submit-only validator process needs exactly one secret: the validator hotkey file. Copy only the hotkey (not the coldkey) onto the node. The submitter expects the hotkey under the validator wallet's `hotkeys/` directory on the configured wallet path. ## If running a validator is too heavy If the validator infrastructure requirements are too high, you do not have to run the submitter yourself. Operators can use the Bittensor CHK / stake weight check flow to give validator power to the recommended subnet validator hotkey instead. See [miners and validators](/concepts/miners-and-validators). ## Next Install the role that matches your keys. What each role does with its hotkey. # Weights and emissions Source: https://docs.joinbase.ai/concepts/weights-and-emissions How BASE turns each challenge's raw scores into a single normalized weight vector for the chain. [Weights](/concepts/glossary#weights) are the scores the subnet publishes to Bittensor. [Emissions](/concepts/glossary#emissions) are how the subnet splits influence across challenges. This page explains how raw challenge scores become one normalized vector. ## The problem the aggregator solves Several [challenges](/concepts/glossary#challenge) run at once, each producing its own raw [hotkey](/concepts/glossary#hotkey) scores on its own scale. Bittensor expects a single vector indexed by [UID](/concepts/glossary#uid). The aggregator bridges the two. ## Step 1: normalize each challenge's weights For one challenge, the raw `hotkey -> weight` map is cleaned and normalized. Non-finite values and values at or below zero are dropped, then the rest are scaled to sum to 1. ```python theme={"dark"} def normalize_weights(raw: dict[str, float]) -> dict[str, float]: cleaned = _clean_weights(raw) total = sum(cleaned.values()) if total <= 0: return {} return {hotkey: value / total for hotkey, value in cleaned.items()} ``` ## Step 2: normalize emissions across challenges Each active challenge carries an emission share (`emission_percent`). The aggregator clamps negatives to zero and normalizes the shares of active challenges so they sum to 1. If nothing is active, every share is zero. ## Step 3: combine into a UID vector The aggregator walks every active challenge. For each one it multiplies that challenge's normalized emission share by each hotkey's normalized weight, and accumulates per hotkey: ```python theme={"dark"} for hotkey, weight in normalize_weights(result.weights).items(): hotkey_scores[hotkey] += emission * weight ``` It then maps each hotkey to its Bittensor UID and sums per UID, with two rules: * A hotkey with no known UID is skipped. * UID `0` is skipped. Finally it normalizes the UID scores to sum to 1, ordered by UID. If the total is zero, it falls back to assigning all weight to UID `0`. The practical takeaway: your reward depends on both how well you score **within** a challenge and that challenge's **emission share**. A strong score in a small-share challenge contributes less than the same relative score in a large-share one. ## What gets published The aggregator emits a `FinalWeights` value with aligned `uids` and `weights` lists, plus the surviving per-hotkey weights. This is the vector the on-chain submitter reads from the master and submits to Bittensor at each [epoch](/concepts/glossary#epoch). ## Next Why the subnet is built to reward real work. See where aggregation sits in the full flow. # OG NOTES Source: https://docs.joinbase.ai/images/OG-NOTES # OG / Social Card — Notes (Task T4) Asset: `/images/og.png` (repo path `images/og.png`) — the Open Graph / social card for the BASE Mintlify docs site. Wired by T2 in `docs.json`. ## Image specs * **Dimensions:** 1200 × 630 px (standard Open Graph 1.91:1). * **Format:** PNG, 8-bit RGB, non-interlaced (\~105 KB). * **Render pipeline:** authored with Pillow at 2× supersample (2400×1260) then Lanczos-downscaled to 1200×630 for crisp anti-aliased edges. ## Colors (from platform-network design system) Sourced from `frontend/src/app/globals.css` + `frontend/tailwind.config.ts`: * **Brand red (accent):** `#d91f35` (`--acid`) * **Background:** `#000` (`--bg`) * **Foreground text:** `#fff` (`--fg`) * Muted text uses white at \~58% / lower alpha (`--muted` = `rgba(255,255,255,0.58)`). * Rail hairlines / corner ticks use `rgba(255,255,255,0.14)` (`--line`). * (Brand `--success` `#b2ff22` intentionally NOT used on the card — red-on-black is the primary identity; lime is a status accent only.) ## Tagline text used > **A Bittensor subnet for decentralized AI research.** Eyebrow label (mono): `BITTENSOR SUBNET // NETUID 100` Sub-label: `decentralized collaborative AI research` Footer: `BASEINTELLIGENCE` (left) · `subnet 100 // bittensor` (right, red) Framing is accurate to the project: BASE is a Bittensor subnet (**netuid 100**) for decentralized collaborative AI research via challenges. ## Brand mark The geometric BASE mark (two stacked horizontal hexagons + a diamond) is taken from `frontend/public/logo.png` and **recolored from its native `#f90324` red to the design-system brand red `#d91f35`** (per-pixel lerp red↔white preserving the white shapes and anti-aliased edges). It appears twice: 1. a crisp 104px tile top-left, and 2. a large low-opacity "ghost" mark bleeding off the right edge for atmosphere. ## Fonts — Google Fonts confirmation (for T2 wiring) Both typefaces are available on Google Fonts. Confirmed by successfully fetching the Google Fonts CSS2 API (`https://fonts.googleapis.com/css2?family=...`) for each and finding the canonical `font-family` declarations and `.ttf` URLs on `fonts.gstatic.com`: | Role | **Exact Google Fonts family name** | Weights available / used | | ---- | ---------------------------------- | --------------------------------------------------------- | | Body | **`Rethink Sans`** | 400, 500, 600, 700 (+800 var.); card uses 500 / ExtraBold | | Mono | **`Space Mono`** | 400, 700; card uses both | T2 should wire these **exact** family names (`Rethink Sans`, `Space Mono`). This matches the existing platform-network usage in `globals.css` line 2: `family=Rethink+Sans:wght@400;500;600;700&family=Space+Mono:wght@400;700`. > NOTE: The card uses ONLY these two Google Fonts. It does **not** reference the > paid serif font / Typekit kit listed in `globals.css` line 1 (which is > explicitly forbidden for this docs project per `CONTRIBUTING-DOCS.md`). ## Verification * `file og.png` → `PNG image data, 1200 x 630, 8-bit/color RGB`. * `Image.open(...).size` → `(1200, 630)`. * Evidence: `/root/.omo/evidence/task-4-og.txt`, `/root/.omo/evidence/task-4-fonts.txt`. # Welcome to BASE Source: https://docs.joinbase.ai/index BASE is a Bittensor subnet (netuid 100): validators run infrastructure; miners compete inside pluggable challenges. BASE is a multi-challenge Bittensor subnet. Independent **challenges** plug into one validator network. The subnet routes miner traffic to the right challenge, collects raw scores, normalizes emissions, maps miner hotkeys to Bittensor UIDs, and publishes the final weight vector for validators to submit on-chain. **Validators run BASE.** **Miners compete inside challenges.** There is no undirected Base mining game UI mixed into validator install docs as the miner path. ```mermaid theme={"dark"} flowchart TB subgraph Validators[BASE operators] M[Master and proxy] W[Weight normalize] S[On-chain submit] end subgraph Challenges AC[Agent Challenge] PR[PRISM] OT[Other challenges] end Miners --> Challenges Challenges -->|raw weights| W M --> Challenges W --> S ``` New here? Read [What is BASE?](/concepts/overview), then pick a role below. ## Choose your path Install and run BASE: master pieces, weights, join, wallet as validator. Miner home: pick a challenge and follow its full submit and eval pack. Phala TDX self-deploy agents (primary miner pack). Architecture and training recipes (primary miner pack). Thin hub: wallet registration and choose-a-challenge links only. Network-level mental model before a role guide. ## Start fast Compute weights locally without submitting on-chain (operator/dev check). Register, choose a challenge, jump to the right pack. # Authentication & signing Source: https://docs.joinbase.ai/miners/authentication Build the canonical upload message, sign it with your hotkey, and set the four required headers. Every upload is authenticated with a hotkey signature. The proxy reconstructs a canonical message from your request, verifies your signature against your hotkey, checks freshness and replay, and resolves your hotkey to a UID (`src/platform_network/security/miner_auth.py:150-198`). This page shows exactly what to sign. ## Required headers You send four headers on every signed upload (`src/platform_network/security/miner_auth.py:159-162`, `docs/miner/README.md:90-95`): ```http theme={"dark"} X-Hotkey: X-Signature: X-Nonce: X-Timestamp: ``` | Header | Meaning | Source | | ------------- | -------------------------------------------- | ----------------------------------------------------- | | `X-Hotkey` | Your ss58 hotkey address | `src/platform_network/security/miner_auth.py:159` | | `X-Signature` | Signature over the canonical message | `src/platform_network/security/miner_auth.py:160` | | `X-Nonce` | Unique per-request value (replay protection) | `src/platform_network/security/miner_auth.py:161` | | `X-Timestamp` | Unix seconds, checked for freshness | `src/platform_network/security/miner_auth.py:162-169` | Any missing header fails with `missing
` (`src/platform_network/security/miner_auth.py:230-234`). ## The canonical message The signed message is built exactly as (`src/platform_network/security/miner_auth.py:96-111`): ```text theme={"dark"} platform-upload-v1:{netuid}:{challenge_slug}:{METHOD}:{path}:{hotkey}:{nonce}:{timestamp}:{body_hash} ``` Where: * `netuid` is `100` (`src/platform_network/config/settings.py:12`). * `challenge_slug` is the resolved challenge slug, for example `agent-challenge` or `prism` (`src/platform_network/master/app_proxy.py:438`). * `METHOD` is the uppercased HTTP method (`src/platform_network/security/miner_auth.py:109`). * `path` is the request path (`src/platform_network/master/app_proxy.py:435`). * `hotkey`, `nonce`, and `timestamp` are the same values as your headers (`src/platform_network/security/miner_auth.py:171-179`). * `body_hash` is the SHA-256 hex digest of the raw request body (`src/platform_network/security/miner_auth.py:170`). ## How to sign Take the SHA-256 of the exact bytes you will upload and hex-encode it (`src/platform_network/security/miner_auth.py:170`). Concatenate the fields with the `platform-upload-v1:` prefix and colon separators, using `netuid` `100`, the challenge slug, the uppercased method, the path, your hotkey, your nonce, your timestamp, and the body hash (`src/platform_network/security/miner_auth.py:96-111`). Sign the message bytes with your hotkey keypair. Verification uses the substrate ss58 keypair `verify` (`src/platform_network/security/miner_auth.py:114-121`). Provide the signature as a hex string, with or without a `0x` prefix (`src/platform_network/security/miner_auth.py:222-227`). Set `X-Hotkey`, `X-Signature`, `X-Nonce`, and `X-Timestamp`, then POST the body (`src/platform_network/security/miner_auth.py:159-162`). ## Freshness and replay * **Freshness:** a timestamp more than `300` seconds from server time fails with `stale signature` (`src/platform_network/security/miner_auth.py:168-169`, `src/platform_network/config/settings.py:33`). * **Replay:** each nonce is reserved per netuid, challenge, and hotkey; a reused nonce fails with `nonce already used` (`src/platform_network/security/miner_auth.py:60-90`). Nonces are retained for `86,400` seconds (`src/platform_network/config/settings.py:34`). * **Timestamp format:** a non-integer timestamp fails with `invalid timestamp` (`src/platform_network/security/miner_auth.py:163-166`). The signature covers the body hash, so any change to the bytes you upload, including a re-zip with a different timestamp inside the archive, changes `body_hash` and invalidates the signature (`src/platform_network/security/miner_auth.py:170-180`). Sign the exact bytes you send. ## Identity resolution After the signature verifies, your hotkey is resolved to a UID from the metagraph (`src/platform_network/security/miner_auth.py:183,200-219`): * An unregistered hotkey fails with `unknown hotkey` (`src/platform_network/security/miner_auth.py:213-215`). * UID `0` is blocked and fails with `blocked uid` (`src/platform_network/security/miner_auth.py:143,217-218`). Use only fake placeholder values in any examples you publish; the live header values are your real hotkey and a real signature (`docs/miner/README.md:88-95`). ## Next steps Apply this signature to the upload endpoints. Resolve signature and replay errors. # Choosing a challenge Source: https://docs.joinbase.ai/miners/choose-a-challenge Compare Agent Challenge, PRISM, and secondary challenges. Each owns its full miner path. You compete in **one challenge at a time** by following that challenge's docs. BASE only routes traffic, discovers challenges, and normalizes weights. Artifact formats and scoring never share a single Base-global path. ## Primary challenges Build a software-engineering agent. Production: miner-funded Phala CPU TDX review then eval, RA-TLS golden key, Terminal-Bench from a baked guest cache. Slug `agent-challenge`. Submit `architecture.py` + `training.py`. Challenge re-executes train under forced init and scores prequential bits-per-byte. Slug `prism`. ### Agent Challenge (summary) * **Artifact:** agent ZIP, root `agent.py` with `class Agent` (build from `baseagent`) * **After upload:** self-deploy measured review CVM, then after verified allow, eval CVM * **Costs:** Phala CVM spend capped on the self-deploy path (default hard joint cap \$20); OpenRouter for review under measured `.rules` * **Not:** Base LLM gateway on the scored path; validator-deployed score jobs for miners Full pack: [Agent Challenge](/challenges/agent-challenge). ### PRISM (summary) * **Artifact:** two distinct scripts in a zip (or directory snapshot) * **Flow:** AST sandbox → LLM hard gate → GPU re-execution → challenge-authored score * **Anti-cheat:** forced random init, no network in container, secret held-out splits, miner-reported metrics ignored Full pack: [PRISM](/challenges/prism/overview). ## Secondary challenges | Challenge | Slug (typical) | Miner angle | Guide | | ---------------- | ------------------ | ----------------------------- | ------------------------------------- | | Bounty | `bounty-challenge` | Owner-reviewed finished work | [Guide](/challenges/bounty-challenge) | | Data Fabrication | `data-fabrication` | Dataset harness quality | [Guide](/challenges/data-fabrication) | | Agent SWE | `agent-swe` | Real-repo fail-to-pass agents | [Guide](/challenges/agent-swe) | Confirm emission shares and active status from: ```bash theme={"dark"} curl -sf "$PROXY_URL/v1/registry" ``` ## Emissions A strong score in one challenge contributes only through that challenge's configured emission share after BASE normalization. Shares are configuration on the live network, not a fixed promise in these docs. Prefer the registry and operator announcements over stale hard-coded percents. ## Next * [Agent Challenge quickstart](/challenges/agent-challenge/quickstart) * [PRISM quickstart](/challenges/prism/quickstart) * [Authentication](/miners/authentication) # Install the miner Source: https://docs.joinbase.ai/miners/installation Set up the subnet tooling and a Bittensor wallet so you can sign and submit work. Miners interact with BASE over signed HTTP, so the install footprint is small: the subnet package (for tooling and the wallet integration) and a Bittensor wallet. There is no separate miner daemon to run (`src/platform_network/cli_app/main.py:57-71`). ## Requirements | Requirement | Value | Source | | ----------- | ---------------- | ----------------------------------------------- | | Python | `>=3.12` | `pyproject.toml:5` | | Bittensor | `>=9` (extra) | `pyproject.toml:28` | | Wallet | coldkey + hotkey | `src/platform_network/config/settings.py:14-15` | | netuid | `100` | `src/platform_network/config/settings.py:12` | ## Install the subnet package The canonical install uses `uv`. From the `base` repository root (`README.md:212`): ```bash theme={"dark"} uv sync --extra dev --extra master ``` This installs the package and pulls in the Bittensor dependency declared by the `master` extra (`pyproject.toml:39`). The Bittensor library is what provides wallet creation, hotkey signing, and on-chain registration. The `bittensor` extra (`pyproject.toml:28`) is what carries the substrate keypair support used to verify your signatures (`src/platform_network/security/miner_auth.py:114-121`). Installing it locally gives you the same `bittensor` library the subnet uses to verify uploads. ## Verify the install The installed console script is named `platform` (`pyproject.toml:43`). Confirm the CLI loads and inspect the registry to confirm you can reach the subnet: ```bash theme={"dark"} platform --help curl -sf "$PROXY_URL/v1/registry" ``` `platform --help` lists the available command groups (`src/platform_network/cli_app/main.py:57-71`). `/v1/registry` is the public registry read served by the proxy (`README.md:425`). `$PROXY_URL` is the public proxy URL. It defaults to `https://chain.platform.network` (`src/platform_network/config/settings.py:21`, `config/master.example.yaml:11`). On a local single-node bring-up the proxy is published on host port `18080` (`README.md:262`). ## What you do not install You do not install or run a miner service. The CLI's runnable components are operator and validator roles: `master proxy`, `master broker`, `master supervisor`, `master weights`, and `validator run` (`src/platform_network/cli_app/main.py:525-571`, `src/platform_network/cli_app/main.py:574-632`, `src/platform_network/cli_app/main.py:784-854`). Your work as a miner is built locally and submitted through the proxy. ## Next steps Create and register your hotkey. Build and submit your first artifact. # Migrating from baseagent Source: https://docs.joinbase.ai/miners/migration-baseagent Move from standalone baseagent runs to Agent Challenge on BASE: signed submit plus Phala TDX self-deploy. `baseagent` remains the recommended ZIP base for Agent Challenge. What changes when you sell scores on BASE is **not** the Harbor `agent:Agent` entrypoint; it is the network identity and the production evaluation path. ## What stays the same * Root `agent.py` with top-level `class Agent` (Harbor `agent:Agent`) * Tool loops aimed at constrained task workspaces * Building from [`BaseIntelligence/baseagent`](https://github.com/BaseIntelligence/baseagent) ## What changes | Concern | Standalone baseagent | Agent Challenge on BASE | | ------------------ | ----------------------------- | ------------------------------------------------------------------------ | | Identity | Local only | Hotkey registered on **netuid 100** | | Upload | Local CLI runs | Signed `POST /submissions` (challenge-local canonical string) | | Production score | Optional local runners | Miner self-deploy on **Phala CPU TDX** (review then eval) | | LLM on scored path | Whatever you hardcode locally | No Base LLM gateway embeds; measured OpenRouter under pins or tools-only | Historical "DeepSeek-only host inject" writeups are **not** the production TEE narrative. See [Configuration](/challenges/agent-challenge/configuration). ## Migration steps Hotkey on netuid 100. [Wallet and registration](/miners/wallet-registration). Remove `BASE_LLM_GATEWAY_*` and non-measured provider secrets from the shipping ZIP. Use agent-challenge `scripts/submit_agent.py` and [Submit](/challenges/agent-challenge/submit). Follow [Quickstart](/challenges/agent-challenge/quickstart) for review/eval CVMs, money cap, and teardown. ## Canonical docs * [Agent Challenge](/challenges/agent-challenge) * [baseagent](/challenges/agent-challenge/baseagent) * [Miner hub](/miners/overview) # Monitoring & logs Source: https://docs.joinbase.ai/miners/monitoring Track submission status, watch leaderboards, and check subnet health through the proxy. After you submit, you track progress through the same public proxy. The subnet exposes status reads, event streams, leaderboards, and health checks; the challenge owns the status and result endpoints behind the proxy (`docs/miner/README.md:125-135`). ## Check submission status Read a submission's status through the bridge status route (`src/platform_network/master/app_proxy.py:514-518`): ```bash theme={"dark"} curl -sf "$PROXY_URL/v1/challenges/{challenge_slug}/submissions/{submission_id}" ``` For Agent Challenge, the frontend status and event routes are (`docs/miner/README.md:44-51`): ```http theme={"dark"} GET /challenges/agent-challenge/submissions/{id}/status GET /challenges/agent-challenge/submissions/{id}/events ``` ## Submission lifecycle (Agent Challenge) An Agent Challenge submission moves through these public states (`docs/miner/README.md:68`): ```text theme={"dark"} analysis_allowed -> waiting_miner_env -> tb_queued -> tb_running ``` At `waiting_miner_env` the submission pauses at the public state `Waiting for miner action`. You must save env vars or confirm none are needed, then launch (`docs/miner/README.md:66-97`). See [Submitting your work](/miners/submitting) for the env routes. ## Watch the leaderboard List submissions and the leaderboard through the proxy (`docs/miner/README.md:50,62`): ```bash theme={"dark"} curl -sf "$PROXY_URL/challenges/agent-challenge/submissions" curl -sf "$PROXY_URL/challenges/agent-challenge/leaderboard" ``` * `/challenges/agent-challenge/submissions` returns the latest `100` submissions, newest-first (`docs/miner/README.md:62`). * `/challenges/agent-challenge/leaderboard` returns one best-scoring row per hotkey (`docs/miner/README.md:62`). Monitor the challenge leaderboard, not only the subnet layer (`docs/miner/README.md:159`). Your score is computed by the challenge; the subnet only normalizes the exported weight (`docs/miner/README.md:137-149`). ## Subnet health and registry The proxy serves health and the registry/weights reads (`README.md:423-426`): ```bash theme={"dark"} curl -sf "$PROXY_URL/health" # proxy health curl -sf "$PROXY_URL/v1/registry" # active challenges curl -sf "$PROXY_URL/v1/weights/latest" # latest normalized weights ``` | Endpoint | Returns | Source | | -------------------- | -------------------------------- | -------------------------------------------------- | | `/health` | proxy liveness `{"status":"ok"}` | `src/platform_network/master/app_proxy.py:320-322` | | `/v1/registry` | active challenge registry | `README.md:425` | | `/v1/weights/latest` | latest normalized weight vector | `README.md:426` | The public proxy blocks `/internal/*`, `/health`, and `/version` on challenge routes, so those are not reachable as challenge paths (`docs/miner/README.md:64`, `src/platform_network/master/app_proxy.py:73,90-99`). The proxy's own top-level `/health` remains available (`src/platform_network/master/app_proxy.py:320-322`). ## Where to verify locally On a local single-node bring-up, the proxy is on host port `18080` and the broker on `18082` (`README.md:262-264`). The challenges are overlay-internal, so reach them through the proxy rather than a direct host port (`README.md:268-285`): ```bash theme={"dark"} curl -sf http://127.0.0.1:18080/health curl -sf http://127.0.0.1:18080/challenges/prism/leaderboard curl -sf http://127.0.0.1:18080/challenges/agent-challenge/leaderboard ``` ## Next steps Diagnose failed submissions. Review the upload protocol. # Miner hub Source: https://docs.joinbase.ai/miners/overview Register on BASE, choose a challenge, then follow that challenge's miner documentation. There is no undirected Base mining game. BASE is a multi-challenge Bittensor subnet (netuid 100). **Validators run BASE infrastructure.** Miners do **not** "mine Base generic." You register a hotkey, pick a challenge, and follow that challenge's complete miner path (submit, evaluation, costs, TEE if any). ```mermaid theme={"dark"} flowchart LR W[Wallet on netuid 100] --> C[Choose challenge] C --> AC[Agent Challenge docs] C --> PR[PRISM docs] C --> O[Other challenge docs] AC --> S[Challenge scores] PR --> S O --> S S --> N[BASE normalizes weights] ``` ## What miners do on BASE | Miner responsibility | Owners | | -------------------------------------------------- | ------------------------------------------------------- | | Hotkey registration on netuid 100 | You + chain | | Artifact format, eval, leaderboards | **The challenge** | | TEE self-deploy, money caps, CLI | **The challenge** (for example Agent Challenge / Phala) | | Proxy routing + weight normalize + on-chain vector | BASE validators / master | ## Start here Create keys and register on netuid 100. [Wallet and registration](/miners/wallet-registration). Compare primary and secondary challenges. [Choose a challenge](/miners/choose-a-challenge). Do not stay on generic subnet install guides meant for validators. Use the challenge pack below. ## Challenge miner packs Full pack: submit, Phala TDX attestation, evaluation, key release, troubleshooting. Full pack: submit architecture and training, scoring, constraints, API. Owner-reviewed project bounties. Dataset-generation harnesses. Real-repo fail-to-pass agent benchmarks. Catalog and challenge-author guides. ## Shared subnet mechanics (thin) These pages describe **cross-challenge** proxy and identity mechanics only. After you pick a challenge, that challenge's pack wins on artifact format and scoring. * [Authentication and signing](/miners/authentication) * [Submitting through the proxy (generic patterns)](/miners/submitting) * [Monitoring](/miners/monitoring) * [Troubleshooting](/miners/troubleshooting) ## What BASE does not mean for miners * No BASE CLI `miner` subcommand that scores you on a Base-global rubric * No requirement to run master dry-run weights as your miner "quickstart" * Contest mechanics (agent ZIP vs two-script PRISM vs bounty URLs) are not shared ## Next Wallet + choose challenge + jump points. If you meant to operate BASE subnet infra. # Miner quickstart Source: https://docs.joinbase.ai/miners/quickstart Register on netuid 100, choose a challenge, then follow that challenge's full miner pack. Miners do not run a dedicated Base miner binary. There is no undirected "start mining on Base" score path. You register a hotkey, choose a challenge, and complete **that** challenge's submit and evaluation flow. ## Prerequisites * Python tooling as required by your chosen challenge repository * A Bittensor wallet with coldkey and hotkey * Hotkey registered on **netuid 100** ## Steps Register on netuid 100. Walkthrough: [Wallet and registration](/miners/wallet-registration). ```bash theme={"dark"} curl -sf "$PROXY_URL/v1/registry" ``` `$PROXY_URL` is the public subnet proxy for your environment. Phala TDX self-deploy path. Two-script architecture + training. Secondary: [Bounty](/challenges/bounty-challenge), [Data Fabrication](/challenges/data-fabrication), [Agent SWE](/challenges/agent-swe). Use the challenge's artifact format and signed routes. Shared proxy/header patterns only: [Submitting](/miners/submitting) and [Authentication](/miners/authentication). Example: Agent Challenge requires post-upload Phala review/eval self-deploy before a production score exists. PRISM owns sandbox, LLM gate, and GPU re-execution. ## Not this path * [Validators quickstart](/validators/overview) installs BASE operator tooling and weight submit * Root [Quickstart](/quickstart) is a **validator/dev** dry-run of master weights, not miner scoring ## Next * [Choose a challenge](/miners/choose-a-challenge) * [Miner hub](/miners/overview) * [Challenges overview](/challenges/overview) # Submitting your work Source: https://docs.joinbase.ai/miners/submitting Generic subnet proxy upload patterns. Prefer each challenge pack for artifact format and post-submit evaluation. BASE routes miner traffic. **Artifact format, post-submit evaluation, and scoring** are challenge-owned. This page only describes common proxy and identity patterns. After you pick a challenge, that pack is authoritative. | Challenge | Miner submit doc | | --------------- | ------------------------------------------------------- | | Agent Challenge | [Submit an agent](/challenges/agent-challenge/submit) | | PRISM | [Submitting to PRISM](/challenges/prism/submit) | | Others | See each guide under [Challenges](/challenges/overview) | ## Upload endpoints (proxy patterns) Many challenges accept traffic through: ```http theme={"dark"} POST /v1/challenges/{challenge_slug}/submissions POST /challenges/{challenge_slug}/submissions GET /v1/challenges/{challenge_slug}/submissions/{id} ``` | Path | Use | | ---------------------------------------- | ---------------------------------------------------------- | | `POST /v1/challenges/{slug}/submissions` | Raw ZIP bridge upload when the proxy verifies and forwards | | `POST /challenges/{slug}/submissions` | Generic passthrough; challenge-owned body (often JSON) | | Status GETs | Challenge-defined status and leaderboard routes | Agent Challenge production miners should follow the **challenge-local** signing string documented in the Agent Challenge pack. That singular string may differ from the subnet-level `platform-upload-v1:...` bridge string used on some `/v1/...` bridge routes. Always match the path you actually call. ## Bridge upload sketch When using the bridged ZIP path, the proxy typically: 1. Checks body size against the configured max (order of millions of bytes; agent ZIPs are often capped lower **inside** the challenge at 1 MiB) 2. Verifies miner signature headers 3. Forwards a verified identity set to the challenge internal bridge ### Common signature headers ```http theme={"dark"} X-Hotkey: X-Signature: X-Nonce: X-Timestamp: ``` Canonical **bridge** message construction for BASE proxy uploads: ```text theme={"dark"} platform-upload-v1:{netuid}:{challenge_slug}:{METHOD}:{path}:{hotkey}:{nonce}:{timestamp}:{body_hash} ``` Details: [Authentication](/miners/authentication). ## Agent Challenge note After Agent Challenge upload, production scoring is **not** complete until miner Phala self-deploy finishes attested review and eval. Host-only env PUT / launch flows are legacy offline relative to TEE production. Use the [Agent Challenge pack](/challenges/agent-challenge). ## Next * [Authentication](/miners/authentication) * [Choose a challenge](/miners/choose-a-challenge) * [Monitoring](/miners/monitoring) # Troubleshooting Source: https://docs.joinbase.ai/miners/troubleshooting Diagnose and fix upload errors: 401, 409, 413, and 502 responses from the proxy. Most miner errors are signature, replay, size, or transport failures from the proxy. This page maps the status codes you will see to their cause and fix. Each status is raised by the proxy or the upload verifier in source. ## Status code reference | Status | Meaning | Source | | ------ | ----------------------------------------------- | -------------------------------------------------- | | `401` | Signature or identity failed (`MinerAuthError`) | `src/platform_network/master/app_proxy.py:442-443` | | `409` | Nonce already used (`NonceReplayError`) | `src/platform_network/master/app_proxy.py:440-441` | | `413` | Submission body too large | `src/platform_network/master/app_proxy.py:427-431` | | `502` | Challenge unavailable (transport failure) | `src/platform_network/master/app_proxy.py:475-478` | | `403` | Proxy path is not allowed | `src/platform_network/master/app_proxy.py:386-390` | ## 401 Unauthorized A `401` means signature verification or identity resolution failed (`src/platform_network/master/app_proxy.py:442-443`). The specific message comes from the verifier: One of `X-Hotkey`, `X-Signature`, `X-Nonce`, or `X-Timestamp` is absent (`src/platform_network/security/miner_auth.py:230-234`). Set all four headers (`src/platform_network/security/miner_auth.py:159-162`). `X-Timestamp` is not an integer (`src/platform_network/security/miner_auth.py:163-166`). Send Unix seconds as an integer. The timestamp is more than `300` seconds from server time (`src/platform_network/security/miner_auth.py:168-169`, `src/platform_network/config/settings.py:33`). Re-sign with a current timestamp and resend promptly. The signature does not verify against the canonical message (`src/platform_network/security/miner_auth.py:181-182`). Re-check the message fields and the body hash; the message is `platform-upload-v1:{netuid}:{slug}:{METHOD}:{path}:{hotkey}:{nonce}:{timestamp}:{body_hash}` (`src/platform_network/security/miner_auth.py:96-111`). See [Authentication & signing](/miners/authentication). Your hotkey is not in the metagraph (`src/platform_network/security/miner_auth.py:213-215`). Register on netuid 100; see [Wallet & registration](/miners/wallet-registration). Your hotkey resolves to UID `0`, which is blocked (`src/platform_network/security/miner_auth.py:143,217-218`). The metagraph could not be read while a registered hotkey is required (`src/platform_network/security/miner_auth.py:204-205,208-210`). Retry once the subnet metagraph is reachable again. ## 409 Conflict (nonce replay) A `409` means the nonce was already used (`src/platform_network/master/app_proxy.py:440-441`, `src/platform_network/security/miner_auth.py:89-90`). Generate a fresh, unique nonce for every request. Nonces are retained for `86,400` seconds (`src/platform_network/config/settings.py:34`). Do not retry a failed upload by replaying the same signed request. Always re-sign with a new nonce, or you will turn a transient error into a `409` (`src/platform_network/security/miner_auth.py:60-90`). ## 413 Request entity too large A `413` means the body exceeded the upload limit (`src/platform_network/master/app_proxy.py:427-431`). The maximum is `7,500,000` bytes (`src/platform_network/config/settings.py:35`). Shrink the artifact below the limit. ## 502 Bad gateway (Agent Challenge) A `502` under `/challenges/agent-challenge/...` is a safe unavailable state from proxy transport handling, not a challenge rejection (`docs/miner/README.md:101-103`). Work the checklist (`docs/miner/README.md:105-112`): A route for `/v1/challenges` alone is not enough; `/challenges` must route to the proxy (`docs/miner/README.md:107`). The proxy must point at the Agent Challenge service and still block `/internal/*`, `/health`, and `/version` (`docs/miner/README.md:108`). Check Agent Challenge health, a running Swarm task, service DNS on the overlay, and the service port (`docs/miner/README.md:109`). The challenge service must run on the manager node and not be stuck pending placement (`docs/miner/README.md:110`). Transport failures become safe `502` responses; challenge-origin non-2xx responses pass through as auth, replay, rate-limit, or validation errors (`docs/miner/README.md:111`). If the failing request is an env action, confirm it uses the signed env routes and includes only the four signed miner header names (`docs/miner/README.md:112`, `docs/miner/README.md:90-95`). See [Submitting your work](/miners/submitting). ## 403 Forbidden (blocked path) A `403` means the path targets a private challenge route (`src/platform_network/master/app_proxy.py:386-390`). The proxy blocks `/internal/*`, `/health`, `/version`, and benchmark-execution paths on challenge routes (`src/platform_network/master/app_proxy.py:73-74,90-110`). Use only the public submission, status, and leaderboard routes. ## Next steps Rebuild a valid signature. Confirm status after a fix. # Wallet & registration Source: https://docs.joinbase.ai/miners/wallet-registration Create a Bittensor coldkey and hotkey, then register your hotkey on netuid 100. To mine on BASE you need a Bittensor wallet (a coldkey for custody and a hotkey for signing) and a hotkey registered on the subnet. The subnet runs on **netuid 100** (`src/platform_network/config/settings.py:12`), and every signed submission is bound to that netuid (`src/platform_network/security/miner_auth.py:96-111`). ## Keys you need | Key | Role | Source | | ----------- | ----------------------- | -------------------------------------------- | | **coldkey** | Custody and ownership | `src/platform_network/config/settings.py:14` | | **hotkey** | Operational signing key | `src/platform_network/config/settings.py:15` | The subnet's default wallet name and default hotkey name are both `default` (`src/platform_network/config/settings.py:14-15`, `config/master.example.yaml:5-6`). Keep your hotkey consistent across submissions so your work accrues to one identity (`docs/miner/README.md:160`). ## Register on netuid 100 Create a coldkey and a hotkey with the Bittensor wallet tooling installed by the `bittensor` extra (`pyproject.toml:28`). The wallet name and hotkey name you choose map to the `wallet_name` and `wallet_hotkey` settings (`src/platform_network/config/settings.py:14-15`). Register your hotkey on the subnet. The netuid is fixed at `100` (`src/platform_network/config/settings.py:12`, `config/master.example.yaml:3`, `config/validator.example.yaml:3`). Registration places your hotkey in the metagraph and assigns it a UID. Uploads require a registered hotkey by default (`src/platform_network/config/settings.py:36`). The subnet maps your hotkey to a UID from the metagraph at verification time (`src/platform_network/security/miner_auth.py:200-219`). If the hotkey is not in the metagraph, the upload is rejected with an `unknown hotkey` error (`src/platform_network/security/miner_auth.py:213-215`). ## Why registration matters for uploads When you submit, the subnet verifies your signature and then resolves your hotkey to a UID (`src/platform_network/security/miner_auth.py:183`, `src/platform_network/security/miner_auth.py:200-219`): * The hotkey must exist in the metagraph, or the upload fails with `unknown hotkey` (`src/platform_network/security/miner_auth.py:213-215`). * UID `0` is blocked (`src/platform_network/security/miner_auth.py:143`); a submission resolving to UID `0` fails with `blocked uid` (`src/platform_network/security/miner_auth.py:217-218`). * If the metagraph is unavailable and a registered hotkey is required, the upload fails with `metagraph unavailable` (`src/platform_network/security/miner_auth.py:204-205`). Registration is enforced because `upload_require_registered_hotkey` defaults to `true` (`src/platform_network/config/settings.py:36`). The verified UID is forwarded to the challenge as `X-Platform-Verified-Uid` (`src/platform_network/master/app_proxy.py:461-462`), so your on-chain identity follows the submission downstream. ## Next steps Pick agent-challenge or PRISM. Sign uploads with your hotkey. # PRISM api (moved) Source: https://docs.joinbase.ai/prism/api Redirect: PRISM documentation lives under Challenges. Moved to [PRISM api](/challenges/prism/api). # PRISM constraints (moved) Source: https://docs.joinbase.ai/prism/constraints Redirect: PRISM documentation lives under Challenges. Moved to [PRISM constraints](/challenges/prism/constraints). # PRISM context (moved) Source: https://docs.joinbase.ai/prism/context Redirect: PRISM documentation lives under Challenges. Moved to [PRISM context](/challenges/prism/context). # PRISM examples (moved) Source: https://docs.joinbase.ai/prism/examples Redirect: PRISM documentation lives under Challenges. Moved to [PRISM examples](/challenges/prism/examples). # PRISM how it works (moved) Source: https://docs.joinbase.ai/prism/how-it-works Redirect: PRISM documentation lives under Challenges. Moved to [PRISM how it works](/challenges/prism/how-it-works). # PRISM operators (moved) Source: https://docs.joinbase.ai/prism/operators Redirect: PRISM documentation lives under Challenges. Moved to [PRISM operators](/challenges/prism/operators). # PRISM overview (moved) Source: https://docs.joinbase.ai/prism/overview Redirect: PRISM documentation lives under Challenges. Moved to [PRISM overview](/challenges/prism/overview). # PRISM quickstart (moved) Source: https://docs.joinbase.ai/prism/quickstart Redirect: PRISM documentation lives under Challenges. Moved to [PRISM quickstart](/challenges/prism/quickstart). # PRISM scoring (moved) Source: https://docs.joinbase.ai/prism/scoring Redirect: PRISM documentation lives under Challenges. Moved to [PRISM scoring](/challenges/prism/scoring). # PRISM submit (moved) Source: https://docs.joinbase.ai/prism/submit Redirect: PRISM documentation lives under Challenges. Moved to [PRISM submit](/challenges/prism/submit). # Quickstart (operators and developers) Source: https://docs.joinbase.ai/quickstart Dry-run BASE master weights with the subnet CLI. Not a miner score path. This quickstart runs the canonical `base` repository locally and computes a weight vector with the CLI in dry-run mode so nothing is submitted on-chain. It validates **operator and developer** tooling. **Miners:** do not treat this as your contest path. Choose a challenge and open its miner pack under [Challenges](/challenges/overview). ## Prerequisites * A Python runtime and the [`uv`](https://docs.astral.sh/uv/) package manager * A clone of the canonical [`base`](https://github.com/BaseIntelligence/base) repository ## Get started From the repository root: ```bash theme={"dark"} uv sync --extra dev --extra master ``` Command groups include `master`, `validator`, `challenge`, `db`, and `registry`. There is no miner score subcommand for undirected Base mining. ```bash theme={"dark"} uv run platform --help ``` ```bash theme={"dark"} uv run platform master weights --once --dry-run ``` Expect `compute-only: computed weights`. See [how it works](/concepts/how-it-works). ## What to do next Full BASE operator guide. Wallet + choose challenge hub. Primary miner pack (TEE). Primary miner pack (NAS). # API overview Source: https://docs.joinbase.ai/reference/api The public HTTP API exposed by the BASE subnet master, plus its authentication models. The subnet master exposes a single public HTTP API. It serves three kinds of routes on one port: the **proxy** routes that bridge miner traffic to active challenges, the public **registry** and **weights** reads that validators consume, and a token-gated **admin** surface for operators. The proxy app is created by `create_proxy_app` and, when an operator wires a runtime controller into it, the admin/registry router is mounted on the same app so everything is served on one port (`app_proxy.py:263`, `app_proxy.py:534-548`). ## Endpoint map | Method | Path | Auth | Page | | -------------------- | ------------------------------------------------------------- | ------------------ | --------------------------------------------- | | `GET` | `/health` | none | below | | `POST` | `/v1/challenges/{challenge_name}/submissions` | miner signature | [Upload protocol](/reference/upload-protocol) | | `GET` | `/v1/challenges/{challenge_name}/submissions/{submission_id}` | none | [Proxy API](/reference/api-proxy) | | `GET` | `/challenges/{slug}` and `/challenges/{slug}/{path}` | passthrough | [Proxy API](/reference/api-proxy) | | `GET` | `/v1/registry` | none (public read) | [Proxy API](/reference/api-proxy) | | `GET` | `/v1/weights/latest` | none (public read) | [Weights API](/reference/api-weights) | | `GET` | `/v1/challenges/dashboard.svg` | none (public read) | [Proxy API](/reference/api-proxy) | | `GET` | `/admin`, `/admin/challenges` | admin token | [Admin API](/reference/api-admin) | | `POST`/`PATCH`/`GET` | `/v1/admin/challenges/*` | admin token | [Admin API](/reference/api-admin) | Routes confirmed at: `/health` (`app_proxy.py:320`), submissions upload (`app_proxy.py:510`), submission status (`app_proxy.py:514`), proxy passthrough (`app_proxy.py:520`, `app_proxy.py:527`), `/v1/registry` (`app_admin.py:132`), `/v1/weights/latest` (`app_admin.py:136`), `/v1/challenges/dashboard.svg` (`app_admin.py:165`), admin pages (`app_admin.py:176`, `app_admin.py:186`), and the `/v1/admin/challenges/*` management routes (`app_admin.py:202`–`app_admin.py:326`). ## Authentication models There are two distinct auth models on the public API, plus internal tokens used only between the master and its containers. ### Miner request signing The signed upload route requires four request headers — `X-Hotkey`, `X-Signature`, `X-Nonce`, and `X-Timestamp` — which are verified against a canonical message before the request is bridged to the challenge (`miner_auth.py:159-162`, `miner_auth.py:96-111`). See [Upload protocol](/reference/upload-protocol) for the full signing scheme. ### Admin token Management routes depend on `require_admin`, which accepts the token either in an `X-Admin-Token` header or as an `Authorization: Bearer ` credential, and compares it in constant time (`app_admin.py:121-130`, `auth.py:28-29`). The expected token is loaded from the `ADMIN_TOKEN` environment variable, or from the file named by `ADMIN_TOKEN_FILE` (`auth.py:10-18`). A mismatch returns `401 Unauthorized` (`app_admin.py:127-130`). See [Admin API](/reference/api-admin). ### Public reads `/v1/registry`, `/v1/weights/latest`, and `/v1/challenges/dashboard.svg` are registered without the `require_admin` dependency, so they are readable without a token (`app_admin.py:132-174`). ## Health check ```bash theme={"dark"} curl -s "$MASTER_URL/health" ``` Returns `{"status": "ok"}` (`app_proxy.py:320-322`). `$MASTER_URL` is the base URL of the subnet master in your deployment. This API covers the subnet master, proxy, weights, and upload surfaces. Challenge applications expose their own routes through the proxy; PRISM's API is documented separately in the PRISM tab. ## Related The control plane that exposes this public API. Where the proxy sits in the subnet topology. # Admin API Source: https://docs.joinbase.ai/reference/api-admin Token-gated and internal endpoints for challenge administration and runtime control on the subnet master. This page documents the operator-facing admin surface and the internal endpoints used between the master and its containers. The `/admin*` and `/v1/admin/*` routes are gated by an admin token and are **not** public. The Docker broker and challenge `get_weights` endpoints are **internal** service-to-service routes — they are not exposed to end users. ## Admin authentication Every management route depends on `require_admin`, which reads the token from an `X-Admin-Token` header or from an `Authorization: Bearer ` credential and compares it in constant time (`app_admin.py:121-130`, `auth.py:28-29`). The expected value comes from the `ADMIN_TOKEN` environment variable, or from the file at `ADMIN_TOKEN_FILE` (`auth.py:10-18`). A mismatch returns `401` (`app_admin.py:127-130`). ```bash theme={"dark"} curl -s "$MASTER_URL/v1/admin/challenges/agent-challenge" \ -H "X-Admin-Token: $ADMIN_TOKEN" ``` ## Admin HTML pages | Method | Path | Returns | Source | | ------ | ------------------- | ------------------------ | ---------------------- | | `GET` | `/admin` | HTML index | `app_admin.py:176-184` | | `GET` | `/admin/challenges` | HTML table of challenges | `app_admin.py:186-200` | Both depend on `require_admin` (`app_admin.py:176`, `app_admin.py:186`). ## Challenge management routes All routes below depend on `require_admin`. ### `POST /v1/admin/challenges` Creates a challenge. Returns `201 Created` (`app_admin.py:202-234`). **Request** — `ChallengeCreate` (`schemas/challenge.py:22-41`). Required fields: `slug` (pattern `^[a-z0-9][a-z0-9-]*[a-z0-9]$`, `schemas/challenge.py:25`), `name`, `image`, `version`. Optional fields include `emission_percent` (default `0`), `status` (default `draft`), `description`, `api_version` (default `"1.0"`), `internal_base_url`, `required_capabilities` (default `["get_weights", "proxy_routes"]`), `resources`, `volumes`, `env`, `secrets`, and `metadata` (`schemas/challenge.py:29-41`). **Response** — `ChallengeCreateResponse` (`schemas/challenge.py:131-136`): | Field | Type | Notes | | --------------------- | ------ | --------------------------------------------------------------- | | `challenge` | object | `ChallengeAdminView` (`schemas/challenge.py:106-128`) | | `challenge_token` | string | Clear token returned exactly once (`schemas/challenge.py:135`) | | `docker_broker_token` | string | Broker token returned exactly once (`schemas/challenge.py:136`) | A duplicate slug returns `409` (`app_admin.py:214-218`); an image that fails the production policy returns `400` (`app_admin.py:219-220`). ```bash theme={"dark"} curl -s -X POST "$MASTER_URL/v1/admin/challenges" \ -H "X-Admin-Token: $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"slug":"agent-challenge","name":"Agent Challenge","image":"registry.example/agent:1.0","version":"1.0"}' ``` ### `GET /v1/admin/challenges/{slug}` Returns a `ChallengeAdminView` (`schemas/challenge.py:106-128`); unknown slug returns `404` (`app_admin.py:236-245`). ```bash theme={"dark"} curl -s "$MASTER_URL/v1/admin/challenges/agent-challenge" \ -H "X-Admin-Token: $ADMIN_TOKEN" ``` ### `PATCH /v1/admin/challenges/{slug}` Updates mutable metadata and returns a `ChallengeAdminView` (`app_admin.py:247-264`). **Request** — `ChallengeUpdate`; all fields optional (`schemas/challenge.py:51-67`). ```bash theme={"dark"} curl -s -X PATCH "$MASTER_URL/v1/admin/challenges/agent-challenge" \ -H "X-Admin-Token: $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{"emission_percent":"25"}' ``` ### Status transitions | Method | Path | Result | Source | | ------ | ---------------------------------------- | ---------------------- | ---------------------- | | `POST` | `/v1/admin/challenges/{slug}/activate` | sets status `active` | `app_admin.py:266-277` | | `POST` | `/v1/admin/challenges/{slug}/deactivate` | sets status `inactive` | `app_admin.py:279-290` | Both return a `ChallengeAdminView` (`schemas/challenge.py:106-128`). ```bash theme={"dark"} curl -s -X POST "$MASTER_URL/v1/admin/challenges/agent-challenge/activate" \ -H "X-Admin-Token: $ADMIN_TOKEN" ``` ### Runtime control | Method | Path | Operation | Source | | ------ | ------------------------------------- | --------- | ---------------------- | | `POST` | `/v1/admin/challenges/{slug}/pull` | `pull` | `app_admin.py:304-310` | | `POST` | `/v1/admin/challenges/{slug}/restart` | `restart` | `app_admin.py:312-318` | | `GET` | `/v1/admin/challenges/{slug}/status` | `status` | `app_admin.py:320-326` | All three return a `RuntimeOperationResponse` (`schemas/challenge.py:168-174`): | Field | Type | | ----------- | -------------- | | `slug` | string | | `operation` | string | | `status` | string | | `detail` | string or null | ```bash theme={"dark"} curl -s -X POST "$MASTER_URL/v1/admin/challenges/agent-challenge/restart" \ -H "X-Admin-Token: $ADMIN_TOKEN" ``` ## Internal service endpoints The following routes are **internal** — they run on separate service apps and are authenticated with per-challenge tokens, not the admin token. They are documented here for operators; they are not part of the public API. ### Docker broker (internal) The Docker broker runs as its own app (`docker_broker.py:653-659`). Each request is authenticated with `Authorization: Bearer ` plus an `X-Platform-Challenge-Slug` header (`_authenticate`, `docker_broker.py:717-730`). | Method | Path | Request | Response | Source | | ------ | -------------------- | ---------------------- | ----------------------- | -------------------------- | | `POST` | `/v1/docker/run` | `BrokerRunRequest` | `BrokerRunResponse` | `docker_broker.py:669-690` | | `POST` | `/v1/docker/cleanup` | `BrokerCleanupRequest` | `BrokerCleanupResponse` | `docker_broker.py:692-701` | | `POST` | `/v1/docker/list` | `BrokerListRequest` | `BrokerListResponse` | `docker_broker.py:703-712` | `POST /v1/docker/run` returns `429` when a workload quota is exceeded (`docker_broker.py:685-688`) and `400` on an executor error (`docker_broker.py:689-690`). `BrokerRunResponse` carries `container_name`, `stdout`, `stderr`, `returncode`, and `timed_out` (`schemas/docker_broker.py:51-56`); `BrokerCleanupResponse` carries `status` (`schemas/docker_broker.py:82-83`); `BrokerListResponse` carries `containers` (`schemas/docker_broker.py:74-75`). ### Challenge weights collection (internal) The master collects per-challenge weights by calling each challenge's `GET /internal/v1/get_weights`, authenticated with the challenge's bearer token and an `X-Platform-Challenge-Slug` header (`challenge_client.py:31-36`). The challenge replies with a `ChallengeWeightsResponse` (`challenge_client.py:43`; see [Weights schema](/reference/weights-schema)). ## Related How the admin token gates these management routes. The internal Docker broker documented on this page. The full endpoint map and authentication models. # Proxy API Source: https://docs.joinbase.ai/reference/api-proxy Public proxy routes on the subnet master: the challenge registry, submission status, and challenge passthrough. The proxy app bridges public traffic to active challenges and exposes the public registry read. This page covers the registry, the submission-status read, and the generic challenge passthrough. The signed submission upload has its own page — see [Upload protocol](/reference/upload-protocol). ## `GET /v1/registry` Returns the validator-facing registry of registered challenges. This is a public read (no `require_admin` dependency) (`app_admin.py:132-134`). **Response** — `RegistryResponse` (`schemas/challenge.py:159-165`): | Field | Type | Notes | | ------------- | ------- | -------------------------------------------------------- | | `network` | string | Defaults to `"platform"` (`schemas/challenge.py:162`) | | `api_version` | string | Defaults to `"1.0"` (`schemas/challenge.py:163`) | | `master_uid` | integer | Defaults to `0` (`schemas/challenge.py:164`) | | `challenges` | array | List of `RegistryChallenge` (`schemas/challenge.py:165`) | Each `RegistryChallenge` (`schemas/challenge.py:139-156`): | Field | Type | | ------------------------ | ----------------------------------------------------------------------------------- | | `slug` | string | | `name` | string | | `image` | string | | `version` | string | | `emission_percent` | number | | `status` | string (`active` / `inactive` / `disabled` / `draft`, `schemas/challenge.py:13-19`) | | `description` | string or null | | `metadata` | object | | `internal_base_url` | string | | `public_proxy_base_path` | string | | `required_capabilities` | array of string | | `resources` | object | | `volumes` | object | | `env` | object | | `secrets` | array of string | Challenges in `draft` status are excluded from this response (`registry.py:411`). The `public_proxy_base_path` defaults to `/challenges/{slug}` (`registry.py:72-75`). ```bash theme={"dark"} curl -s "$MASTER_URL/v1/registry" ``` ## `GET /v1/challenges/{challenge_name}/submissions/{submission_id}` Reads the status of a previously uploaded submission. The proxy forwards the request to the active challenge's `/v1/submissions/{submission_id}` route and returns the upstream response unchanged (`app_proxy.py:514-518`, `app_proxy.py:486-508`, `app_proxy.py:494`). This read does not require a miner signature — it forwards with the standard safe header set (`app_proxy.py:497`). If the challenge is not registered or not `active`, the proxy returns `404` (`app_proxy.py:243-254`); if the challenge is unreachable, it returns `502` (`app_proxy.py:499-502`). ```bash theme={"dark"} curl -s "$MASTER_URL/v1/challenges/agent-challenge/submissions/$SUBMISSION_ID" ``` The response body shape is owned by the target challenge and is not defined in the master, so it is not documented here. ## Challenge passthrough Two catch-all routes forward public challenge traffic to the active challenge's internal base URL: * `/challenges/{slug}` (`app_proxy.py:520-525`) * `/challenges/{slug}/{path}` (`app_proxy.py:527-532`) Both accept `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`, and `HEAD` (`app_proxy.py:522`, `app_proxy.py:529`). The proxy adds an `X-Platform-Proxy: true` header and an `X-Platform-Challenge-Slug` header to the forwarded request (`app_proxy.py:185`, `app_proxy.py:404`). ### Blocked paths The passthrough refuses to forward to private challenge routes and returns `403 Forbidden` (`app_proxy.py:386-390`). Blocked paths are (`is_blocked_proxy_path`, `app_proxy.py:90-110`): * `/health` and `/version` (`app_proxy.py:73`) * `/internal` and any `/internal/...` path (`app_proxy.py:96-97`) * benchmark-execution paths such as `/benchmark-executions` or a trailing `run` / `execute` / `launch` action under `benchmark`/`benchmarks` (`app_proxy.py:74`, `app_proxy.py:102-110`) ```bash theme={"dark"} curl -s "$MASTER_URL/challenges/agent-challenge/leaderboard" ``` If the challenge is not registered or not `active`, the passthrough returns `404` (`app_proxy.py:243-254`); upstream transport failures return `502` (`app_proxy.py:414-422`). ## `GET /v1/challenges/dashboard.svg` Returns a rendered challenges dashboard as `image/svg+xml` with `Cache-Control: no-store`. This is a public read (`app_admin.py:165-174`). ```bash theme={"dark"} curl -s "$MASTER_URL/v1/challenges/dashboard.svg" ``` ## Related The proxy and registry internals behind these routes. How passthrough traffic reaches an active challenge. The full endpoint map and authentication models. # Weights API Source: https://docs.joinbase.ai/reference/api-weights The public weights read on the subnet master: the aggregated, chain-ready weight vector for the subnet. The master computes and serves the aggregated weight vector for the subnet. This is a public read used by validators (`app_admin.py:136-157`). ## `GET /v1/weights/latest` Returns the latest aggregated `MasterWeightsResponse` for the subnet. The master gathers active-challenge inputs and computes the response over the configured `netuid` and chain endpoint (`app_admin.py:144-151`). This is a public read (no `require_admin` dependency, `app_admin.py:136`). ```bash theme={"dark"} curl -s "$MASTER_URL/v1/weights/latest" ``` **Response** — `MasterWeightsResponse` (`schemas/weights.py:33-42`): | Field | Type | Notes | | ---------------------- | ---------------- | ----------------------------------------------------------------------------------------------- | | `netuid` | integer | Subnet id; `100` for BASE (`config/settings.py:12`) | | `chain_endpoint` | string | Chain endpoint used for the computation (`schemas/weights.py:35`) | | `uids` | array of integer | UIDs in the weight vector (`schemas/weights.py:36`) | | `weights` | array of number | Weights aligned to `uids` (`schemas/weights.py:37`) | | `hotkey_weights` | object | Map of hotkey to weight (`schemas/weights.py:38`) | | `computed_at` | datetime | When the vector was computed (`schemas/weights.py:39`) | | `expires_at` | datetime | Freshness deadline; must be in the future (`schemas/weights.py:40`, `schemas/weights.py:44-48`) | | `source_challenges` | array | Per-challenge contributions, `ChallengeWeightsResult` (`schemas/weights.py:41`) | | `metagraph_updated_at` | datetime | When the metagraph snapshot was taken (`schemas/weights.py:42`) | Each entry in `source_challenges` is a `ChallengeWeightsResult` (`schemas/weights.py:19-24`): | Field | Type | Notes | | ------------------ | -------------- | ---------------------------------------------------------------------- | | `slug` | string | Challenge slug (`schemas/weights.py:20`) | | `emission_percent` | number | Challenge emission share (`schemas/weights.py:21`) | | `weights` | object | Per-hotkey weights from the challenge (`schemas/weights.py:22`) | | `ok` | boolean | Whether collection succeeded; default `true` (`schemas/weights.py:23`) | | `error` | string or null | Error detail when `ok` is false (`schemas/weights.py:24`) | The full field-level schema, including the related `ChallengeWeightsResponse` and `FinalWeights` models, is on the [Weights schema](/reference/weights-schema) page. ## Error responses | Status | When | Source | | ------ | ---------------------------------------- | ---------------------- | | `503` | The master weight service is unavailable | `app_admin.py:138-142` | | `502` | Computation raised an unexpected error | `app_admin.py:154-157` | ## Freshness The master weights freshness window is defined by `MASTER_WEIGHTS_FRESHNESS_SECONDS = 720` (`schemas/weights.py:8`). The `expires_at` field is validated to be in the future when the response is built (`schemas/weights.py:44-48`). ## Related How this vector is computed and normalized each epoch. The submit-only process that posts this vector on-chain. The field-level models behind this response. # CLI overview Source: https://docs.joinbase.ai/reference/cli The platform command: install, command groups, and the global config option shared by every subcommand. Every node role on BASE is driven by one command-line tool. This page maps the full command tree and the conventions every subcommand shares. Each command group has its own reference page linked below. ## The `platform` command The package `platform-network` installs a single console script named `platform` (`base/pyproject.toml:43`), wired to the Typer application in `base/src/platform_network/cli.py:1`. The root app's help string is `Platform Network multi-challenge subnet CLI` (`base/src/platform_network/cli_app/main.py:57`). Install the package, then call `platform`: ```bash theme={"dark"} pip install platform-network platform --help ``` The optional extras `master`, `validator`, and `bittensor` pull in the Bittensor SDK needed for on-chain runtimes (`base/pyproject.toml:28-40`). Python 3.12 or newer is required (`base/pyproject.toml:5`). ## Command groups The root app registers five command groups (`base/src/platform_network/cli_app/main.py:67-71`). The `master` group registers two nested subgroups, `challenges` and `worker` (`base/src/platform_network/cli_app/main.py:65-66`). | Group | Invocation | Purpose | Reference | | ----------- | ------------------------ | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | `master` | `platform master ...` | Run the control plane (proxy, broker, supervisor, weights) and manage Swarm workers and challenge records. | [Master CLI](/reference/cli-master) | | `validator` | `platform validator ...` | Run the normal validator runtime. | [Validator CLI](/reference/cli-validator) | | `challenge` | `platform challenge ...` | Scaffold and manage challenge records. | [Challenge CLI](/reference/cli-challenge) | | `db` | `platform db ...` | Apply and author database migrations. | [DB & registry CLI](/reference/cli-db-registry) | | `registry` | `platform registry ...` | Inspect the challenge registry a validator sees. | [DB & registry CLI](/reference/cli-db-registry) | Group help text, verbatim from source: | Group | Help string | Source | | ------------------- | ---------------------------------------- | ---------------------------------------------- | | `master` | Run master components | `base/src/platform_network/cli_app/main.py:58` | | `master challenges` | Manage master challenge records | `base/src/platform_network/cli_app/main.py:59` | | `master worker` | Manage Swarm workers (CPU/GPU job nodes) | `base/src/platform_network/cli_app/main.py:64` | | `validator` | Run normal validator components | `base/src/platform_network/cli_app/main.py:60` | | `challenge` | Manage and scaffold challenges | `base/src/platform_network/cli_app/main.py:61` | | `db` | Database helpers | `base/src/platform_network/cli_app/main.py:62` | | `registry` | Registry helpers | `base/src/platform_network/cli_app/main.py:63` | ## The global `--config` option Most commands take a single `--config` option pointing at a YAML settings file. The defaults differ by role: * Master-side and challenge-admin commands default to `config/master.example.yaml`. Example: `platform master proxy` (`base/src/platform_network/cli_app/main.py:526`). * Validator-side commands default to `config/validator.example.yaml`. Example: `platform validator run` (`base/src/platform_network/cli_app/main.py:835`) and `platform registry print` (`base/src/platform_network/cli_app/main.py:944`). The file is loaded by `load_settings()`, which reads the YAML mapping and then overlays environment variables (`base/src/platform_network/config/loader.py:37-47`). A missing file raises an error rather than falling back to defaults (`base/src/platform_network/config/loader.py:41-42`). See [Configuration overview](/reference/configuration) for the loading model and [Settings reference](/reference/settings) for every key. ```bash theme={"dark"} platform master proxy --config /etc/platform/master.yaml platform validator run --config /etc/platform/validator.yaml ``` ## Full command tree * `platform master` * `proxy` — run the public proxy + admin/registry API (`base/src/platform_network/cli_app/main.py:525-526`) * `broker` — run the Docker broker API (`base/src/platform_network/cli_app/main.py:574-575`) * `supervisor` — run the Swarm control-plane supervisor (`base/src/platform_network/cli_app/main.py:621-622`) * `refresh-challenge-images` — re-pin challenge images to current digests (`base/src/platform_network/cli_app/main.py:717-721`) * `weights` — compute (and optionally submit) master weights (`base/src/platform_network/cli_app/main.py:784-794`) * `challenges seed-prism` — seed/update the PRISM and Agent Challenge records (`base/src/platform_network/cli_app/main.py:765-768`) * `worker token` — print a `docker swarm join` command (`base/src/platform_network/cli_app/main.py:652-657`) * `worker list` — list Swarm nodes (`base/src/platform_network/cli_app/main.py:667-669`) * `worker label` — label a node cpu/gpu (`base/src/platform_network/cli_app/main.py:673-678`) * `worker drain` — drain or reactivate a node (`base/src/platform_network/cli_app/main.py:686-693`) * `worker rm` — remove a node (`base/src/platform_network/cli_app/main.py:698-703`) * `worker inspect` — inspect a node (`base/src/platform_network/cli_app/main.py:711-713`) * `platform validator` * `run` — run the normal validator runtime (`base/src/platform_network/cli_app/main.py:834-835`) * `platform challenge` * `create` — scaffold a challenge template (`base/src/platform_network/cli_app/main.py:857-865`) * `register` — register a challenge via the admin API (`base/src/platform_network/cli_app/main.py:873-880`) * `activate` — activate a challenge (`base/src/platform_network/cli_app/main.py:894-897`) * `deactivate` — deactivate a challenge (`base/src/platform_network/cli_app/main.py:901-904`) * `pull` — pull a challenge image (`base/src/platform_network/cli_app/main.py:908-911`) * `restart` — restart a challenge (`base/src/platform_network/cli_app/main.py:915-918`) * `platform db` * `migrate` — upgrade the master database (`base/src/platform_network/cli_app/main.py:922-923`) * `revision` — autogenerate an Alembic revision (`base/src/platform_network/cli_app/main.py:930-931`) * `platform registry` * `print` — fetch and print the registry as JSON (`base/src/platform_network/cli_app/main.py:943-944`) Run `platform --help` or `platform --help` to see Typer's generated usage for any command. The tables on the per-group reference pages list every option, its default, and the source line that defines it. ## Related How the master, validators, and challenges fit together. How every `platform` command loads and merges its settings. # Challenge CLI Source: https://docs.joinbase.ai/reference/cli-challenge platform challenge commands: scaffold a new challenge repo and register, activate, pull, or restart challenges on the master. The `platform challenge` group scaffolds new challenges and manages their lifecycle on a running master. Its group help is `Manage and scaffold challenges` (`base/src/platform_network/cli_app/main.py:61`). There are two kinds of command here: * **`create`** runs locally and only writes files — no master required. * **`register`, `activate`, `deactivate`, `pull`, `restart`** call the master admin API and require a configured admin token. ## platform challenge create Scaffolds a new challenge repository from the built-in template (`base/src/platform_network/cli_app/main.py:857-870`). | Argument / Option | Required | Default | Source | | ----------------- | -------- | ----------------------------------- | ----------------------------------------------- | | `slug` | yes | — | `base/src/platform_network/cli_app/main.py:859` | | `--out` | yes | — (destination challenge repo path) | `base/src/platform_network/cli_app/main.py:860` | | `name` | no | derived from slug | `base/src/platform_network/cli_app/main.py:861` | | `image` | no | none | `base/src/platform_network/cli_app/main.py:862` | | `version` | no | `0.1.0` | `base/src/platform_network/cli_app/main.py:863` | | `overwrite` | no | `False` | `base/src/platform_network/cli_app/main.py:864` | ```bash theme={"dark"} platform challenge create my-challenge --out ./my-challenge ``` ## Admin lifecycle commands Each of these posts to the master admin API at `master.registry_url` with an `X-Admin-Token` header (`base/src/platform_network/cli_app/main.py:99-100`). They share the `--config` option, defaulting to `config/master.example.yaml`. Creates a challenge record on the master via `POST /v1/admin/challenges` (`base/src/platform_network/cli_app/main.py:881-891`). The record `version` is taken from the image tag, or `latest` when the image has no tag (`base/src/platform_network/cli_app/main.py:888`). | Argument / Option | Required | Default | Source | | ----------------- | -------- | ---------------------------- | ----------------------------------------------- | | `slug` | yes | — | `base/src/platform_network/cli_app/main.py:875` | | `image` | yes | — | `base/src/platform_network/cli_app/main.py:876` | | `emission` | yes | — (float) | `base/src/platform_network/cli_app/main.py:877` | | `name` | no | slug | `base/src/platform_network/cli_app/main.py:878` | | `--config` | no | `config/master.example.yaml` | `base/src/platform_network/cli_app/main.py:879` | ```bash theme={"dark"} platform challenge register my-challenge ghcr.io//my-challenge:1.0.0 15 ``` Toggle a registered challenge on or off. `activate` posts `/v1/admin/challenges/{slug}/activate` (`base/src/platform_network/cli_app/main.py:894-898`); `deactivate` posts `/v1/admin/challenges/{slug}/deactivate` (`base/src/platform_network/cli_app/main.py:901-905`). | Argument / Option | Required | Default | Source | | ----------------- | -------- | ---------------------------- | ------------------------------------------------------- | | `slug` | yes | — | `base/src/platform_network/cli_app/main.py:896`, `:903` | | `--config` | no | `config/master.example.yaml` | `base/src/platform_network/cli_app/main.py:896`, `:903` | ```bash theme={"dark"} platform challenge activate my-challenge platform challenge deactivate my-challenge ``` Triggers the master to pull the challenge's image via `POST /v1/admin/challenges/{slug}/pull` (`base/src/platform_network/cli_app/main.py:908-912`). | Argument / Option | Required | Default | Source | | ----------------- | -------- | ---------------------------- | ----------------------------------------------- | | `slug` | yes | — | `base/src/platform_network/cli_app/main.py:910` | | `--config` | no | `config/master.example.yaml` | `base/src/platform_network/cli_app/main.py:910` | ```bash theme={"dark"} platform challenge pull my-challenge ``` Restarts the challenge runtime via `POST /v1/admin/challenges/{slug}/restart` (`base/src/platform_network/cli_app/main.py:915-919`). | Argument / Option | Required | Default | Source | | ----------------- | -------- | ---------------------------- | ----------------------------------------------- | | `slug` | yes | — | `base/src/platform_network/cli_app/main.py:917` | | `--config` | no | `config/master.example.yaml` | `base/src/platform_network/cli_app/main.py:917` | ```bash theme={"dark"} platform challenge restart my-challenge ``` The admin commands read the admin token from `security.admin_token` or `security.admin_token_file` (`base/src/platform_network/cli_app/main.py:77-80`). Set one of these before calling `register`, `activate`, `deactivate`, `pull`, or `restart`. See [Settings reference](/reference/settings). ## Related How a challenge plugs into the master and registry. The end-to-end guide to scaffolding and shipping a challenge. The admin routes these lifecycle commands call. # DB & registry CLI Source: https://docs.joinbase.ai/reference/cli-db-registry platform db and platform registry commands: run database migrations, create revisions, and print the live challenge registry. The `platform db` group manages the master database schema, and `platform registry` inspects the live registry served to validators. Their group help is `Database helpers` and `Registry helpers` respectively (`base/src/platform_network/cli_app/main.py:62-63`). ## Database commands Applies all pending Alembic migrations, upgrading the master database to `head` (`base/src/platform_network/cli_app/main.py:922-927`). The runtime database URL comes from `database.url` in your config (`base/src/platform_network/cli_app/main.py:926-927`). | Option | Default | Source | | ---------- | ---------------------------- | ----------------------------------------------- | | `--config` | `config/master.example.yaml` | `base/src/platform_network/cli_app/main.py:923` | ```bash theme={"dark"} platform db migrate --config /etc/platform/master.yaml ``` The underlying helper upgrades to revision `head` by default (`base/src/platform_network/db/migrations.py:21-26`). A matching `downgrade` helper targets `-1` (one revision back) but is not exposed as a CLI command (`base/src/platform_network/db/migrations.py:29-34`). Generates a new Alembic revision with `autogenerate=True` from the current models (`base/src/platform_network/cli_app/main.py:930-940`). | Argument | Required | Source | | --------- | -------- | ----------------------------------------------- | | `message` | yes | `base/src/platform_network/cli_app/main.py:931` | ```bash theme={"dark"} platform db revision "add challenge routes" ``` ## Registry command Fetches the active challenge registry from `validator.registry_url` and prints it as indented JSON (`base/src/platform_network/cli_app/main.py:943-948`). | Option | Default | Source | | ---------- | ------------------------------- | ----------------------------------------------- | | `--config` | `config/validator.example.yaml` | `base/src/platform_network/cli_app/main.py:944` | ```bash theme={"dark"} platform registry print --config /etc/platform/validator.yaml ``` ## Database tables The master schema is defined as SQLAlchemy models (`base/src/platform_network/db/models.py`). `platform db migrate` creates and maintains these tables. | Table | Purpose | Source | | ------------------------- | ------------------------------------------------------------------------------------------- | -------------------------------------------- | | `challenges` | Registered challenges and their lifecycle status, emission percent, version, and metadata. | `base/src/platform_network/db/models.py:58` | | `challenge_images` | Container image coordinates (registry, repository, tag, digest, pull policy) per challenge. | `base/src/platform_network/db/models.py:145` | | `challenge_auth` | Hashed auth and broker tokens for a challenge's internal endpoints. | `base/src/platform_network/db/models.py:172` | | `challenge_resources` | Named runtime resource values requested by a challenge. | `base/src/platform_network/db/models.py:201` | | `challenge_volumes` | Docker volume mounts requested by a challenge. | `base/src/platform_network/db/models.py:226` | | `challenge_secrets` | File secrets mounted into a challenge container. | `base/src/platform_network/db/models.py:252` | | `challenge_env` | Environment variable definitions (encrypted values, secret flag) for a challenge. | `base/src/platform_network/db/models.py:278` | | `challenge_capabilities` | Named capabilities advertised by a challenge. | `base/src/platform_network/db/models.py:304` | | `challenge_routes` | Public route prefixes exposed by a challenge through the proxy. | `base/src/platform_network/db/models.py:329` | | `challenge_health_events` | Historical health and version observations per challenge. | `base/src/platform_network/db/models.py:356` | | `miner_request_nonces` | Replay protection for signed miner uploads accepted by the proxy. | `base/src/platform_network/db/models.py:387` | ### Challenge status values The `challenges.status` column uses the `ChallengeStatus` enum (`base/src/platform_network/db/models.py:30-36`). | Value | Source | | ---------- | ------------------------------------------- | | `active` | `base/src/platform_network/db/models.py:33` | | `inactive` | `base/src/platform_network/db/models.py:34` | | `disabled` | `base/src/platform_network/db/models.py:35` | | `draft` | `base/src/platform_network/db/models.py:36` | New challenges default to `draft` and must be activated before validators evaluate them (`base/src/platform_network/db/models.py:78-79`). See the [Challenge CLI](/reference/cli-challenge) for `activate` / `deactivate`. ## Related The schema and tables these migrations create and maintain. The public `/v1/registry` read this CLI mirrors. # Master CLI Source: https://docs.joinbase.ai/reference/cli-master platform master commands: run the proxy, broker, and supervisor, refresh and seed challenges, compute weights, and manage Swarm workers. The `platform master` group runs the control-plane services and manages the Swarm that executes challenge work. The group help is `Run master components` (`base/src/platform_network/cli_app/main.py:58`). Every command below takes `--config`, defaulting to `config/master.example.yaml` unless noted. ## Service commands These commands start long-running services. Each loads settings, configures JSON logging from `observability.log_json`, and binds to the host/port from your config. Runs the single public API. The proxy app also serves the admin and registry routers, so there is no separate admin listener (`base/src/platform_network/cli_app/main.py:540-543`). On startup it applies database migrations, then binds to `master.proxy_host:master.proxy_port` (`base/src/platform_network/cli_app/main.py:531`, `:569-571`). | Option | Default | Source | | ---------- | ---------------------------- | ----------------------------------------------- | | `--config` | `config/master.example.yaml` | `base/src/platform_network/cli_app/main.py:526` | Default bind address is `0.0.0.0:8081` (`base/src/platform_network/config/settings.py:26-27`). ```bash theme={"dark"} platform master proxy --config /etc/platform/master.yaml ``` Runs the Docker broker that dispatches each evaluation as a short-lived Swarm job. It applies migrations, builds the Swarm broker service from your `docker.*` settings, then binds to `docker.broker_host:docker.broker_port` (`base/src/platform_network/cli_app/main.py:580`, `:614-618`). | Option | Default | Source | | ---------- | ---------------------------- | ----------------------------------------------- | | `--config` | `config/master.example.yaml` | `base/src/platform_network/cli_app/main.py:575` | Default bind address is `0.0.0.0:8082` (`base/src/platform_network/config/settings.py:63-64`). ```bash theme={"dark"} platform master broker --config /etc/platform/master.yaml ``` Runs the Swarm control-plane supervisor as a systemd `Type=notify` service (`base/src/platform_network/cli_app/main.py:621-623`). It builds the supervisor from settings, reports the number of scheduled tasks, and runs until exit (`base/src/platform_network/cli_app/main.py:628-632`). | Option | Default | Source | | ---------- | ---------------------------- | ----------------------------------------------- | | `--config` | `config/master.example.yaml` | `base/src/platform_network/cli_app/main.py:622` | ```bash theme={"dark"} platform master supervisor --config /etc/platform/master.yaml ``` ## Maintenance commands Computes the master weight vector for the current epoch. By default it runs in a loop on the interval `master.epoch_interval_seconds`; pass `--once` to run a single epoch (`base/src/platform_network/cli_app/main.py:815-818`). | Option | Default | Description | Source | | ------------------- | ---------------------------- | ------------------------------------------------------------------- | --------------------------------------------------- | | `--config` | `config/master.example.yaml` | Settings file. | `base/src/platform_network/cli_app/main.py:786` | | `--once` / `--loop` | `--loop` | Run a single epoch, or loop. | `base/src/platform_network/cli_app/main.py:787` | | `--dry-run` | off | Compute only; never submit. | `base/src/platform_network/cli_app/main.py:788` | | `--submit-on-chain` | off | Unsafe compatibility path: submit computed master weights on-chain. | `base/src/platform_network/cli_app/main.py:789-793` | ```bash theme={"dark"} platform master weights --config /etc/platform/master.yaml --once --dry-run ``` Walks the registry, resolves the remote digest for each active challenge image on the given `--tag`, and updates plus restarts challenges whose digest changed (`base/src/platform_network/cli_app/main.py:736-762`). Draft and disabled challenges are skipped (`base/src/platform_network/cli_app/main.py:744`). | Option | Default | Source | | ---------- | ---------------------------- | ----------------------------------------------- | | `--config` | `config/master.example.yaml` | `base/src/platform_network/cli_app/main.py:719` | | `--tag` | `latest` | `base/src/platform_network/cli_app/main.py:720` | ```bash theme={"dark"} platform master refresh-challenge-images --tag latest ``` Creates or updates the PRISM challenge record and, if present, updates the Agent Challenge record (`base/src/platform_network/cli_app/main.py:765-781`). PRISM is seeded at 30% emission and Agent Challenge at 15% (`base/src/platform_network/cli_app/main.py:292-293`). | Option | Default | Source | | ---------- | ---------------------------- | ----------------------------------------------- | | `--config` | `config/master.example.yaml` | `base/src/platform_network/cli_app/main.py:767` | ```bash theme={"dark"} platform master challenges seed-prism ``` ## Swarm worker management The `platform master worker` subgroup wraps `docker` Swarm node commands so you can enroll, label, and retire job nodes. The group help is `Manage Swarm workers (CPU/GPU job nodes)` (`base/src/platform_network/cli_app/main.py:64`). | Command | Arguments and options | Behavior | Source | | ---------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | --------------------------------------------------- | | `worker token` | `--role` (default `worker`; `worker` or `manager`), `--rotate` (rotate the join token first) | Prints the `docker swarm join` command for a new node. | `base/src/platform_network/cli_app/main.py:652-664` | | `worker list` | none | Lists Swarm nodes (`docker node ls`). | `base/src/platform_network/cli_app/main.py:667-670` | | `worker label` | ``, `--workload` (required; `cpu` or `gpu`) | Adds the `platform.workload=` label so the broker schedules jobs onto the node. | `base/src/platform_network/cli_app/main.py:673-683` | | `worker drain` | ``, `--active` | Drains a node, or with `--active` restores `availability=active`. | `base/src/platform_network/cli_app/main.py:686-695` | | `worker rm` | ``, `--force` | Removes a node from the Swarm (`docker node rm`). | `base/src/platform_network/cli_app/main.py:698-708` | | `worker inspect` | `` | Inspects a Swarm node (`docker node inspect`). | `base/src/platform_network/cli_app/main.py:711-714` | ```bash theme={"dark"} # Mint a worker join token on the manager platform master worker token --role worker # After the node joins, label it for CPU jobs platform master worker list platform master worker label --workload cpu ``` `worker token` selects the **Swarm role** with `--role worker|manager`. The **workload class** (cpu/gpu) is set separately with `worker label --workload cpu|gpu` after the node joins (`base/src/platform_network/cli_app/main.py:676`). ## Related The control-plane services these commands run. How worker nodes execute challenge and evaluation jobs. The annotated `master.example.yaml` these commands load. Every `master.*` and `docker.*` key. # Validator CLI Source: https://docs.joinbase.ai/reference/cli-validator platform validator run: start a normal validator that evaluates challenges and submits weights on-chain. The `platform validator` group runs the normal validator. Its group help is `Run normal validator components` (`base/src/platform_network/cli_app/main.py:60`). ## platform validator run Starts a validator that fetches the active challenge set from the registry, runs evaluations through the challenge orchestrator, and submits computed weights on-chain on a fixed interval (`base/src/platform_network/cli_app/main.py:834-854`). | Option | Default | Source | | ---------- | ------------------------------- | ----------------------------------------------- | | `--config` | `config/validator.example.yaml` | `base/src/platform_network/cli_app/main.py:835` | ```bash theme={"dark"} platform validator run --config /etc/platform/validator.yaml ``` ## What `run` reads from config All values come from the `validator.*` section of your settings file (`base/src/platform_network/config/settings.py:41-52`). | Setting | Default | Role in `run` | Source | | --------------------------- | -------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | `registry_url` | `https://chain.platform.network` | Endpoint the validator polls for the active challenge set. | `base/src/platform_network/config/settings.py:42` | | `registry_retry_seconds` | `15` | Backoff between registry fetch retries. | `base/src/platform_network/config/settings.py:43` | | `weights_url` | `None` | Override endpoint for fetching master weights; falls back to `registry_url` when unset. | `base/src/platform_network/config/settings.py:44`, `:50-52` | | `weights_interval_seconds` | `360` | How often `run` submits the latest weights. | `base/src/platform_network/config/settings.py:45` | | `weights_timeout_seconds` | `15.0` | HTTP timeout when fetching weights. | `base/src/platform_network/config/settings.py:46` | | `weights_retries` | `3` | Retry count for the weights client. | `base/src/platform_network/config/settings.py:47` | | `weights_freshness_seconds` | `720` | Maximum age of weights the validator will accept before treating them as stale. | `base/src/platform_network/config/settings.py:48` | The effective weights endpoint is `weights_url` if set, otherwise `registry_url` (`base/src/platform_network/config/settings.py:50-52`). Leave `weights_url` empty to read weights from the same host that serves the registry. ## Wallet and chain `validator run` builds an on-chain submit runtime, so it needs valid wallet and chain settings from the `network.*` section. See [Settings reference](/reference/settings) for `network.wallet_name`, `network.wallet_hotkey`, `network.chain_endpoint`, and `network.netuid` (`base/src/platform_network/config/settings.py:10-17`). See the annotated `validator.example.yaml` for a complete, ready-to-edit configuration. ## Related The submit-only on-chain process `validator run` starts. The full operational guide for running a validator. The weight vector the validator reads and submits. # Configuration examples Source: https://docs.joinbase.ai/reference/config-examples Annotated master and validator configuration files you can copy and adapt. These examples mirror the shipped `config/master.example.yaml` and `config/validator.example.yaml`. Image registry namespaces are shown as `` — replace them with your own fully-qualified registry prefix. See the [Settings reference](/reference/settings) for every available key. Use this for `platform master proxy`, `broker`, `supervisor`, and `weights`. The full source is `base/config/master.example.yaml`. ```yaml theme={"dark"} network: name: platform # base/config/master.example.yaml:2 netuid: 100 # base/config/master.example.yaml:3 chain_endpoint: null # base/config/master.example.yaml:4 wallet_name: default # base/config/master.example.yaml:5 wallet_hotkey: default # base/config/master.example.yaml:6 wallet_path: null # base/config/master.example.yaml:7 master_uid: 0 # base/config/master.example.yaml:8 master: registry_url: https://chain.platform.network # base/config/master.example.yaml:11 proxy_host: 0.0.0.0 # base/config/master.example.yaml:12 proxy_port: 8081 # base/config/master.example.yaml:13 epoch_interval_seconds: 360 # base/config/master.example.yaml:14 metagraph_cache_ttl_seconds: 300 # base/config/master.example.yaml:15 registry_state_file: /var/lib/platform/registry.json # base/config/master.example.yaml:16 validator: registry_url: https://chain.platform.network # base/config/master.example.yaml:19 registry_retry_seconds: 15 # base/config/master.example.yaml:20 weights_url: null # base/config/master.example.yaml:21 weights_interval_seconds: 360 # base/config/master.example.yaml:22 weights_timeout_seconds: 15.0 # base/config/master.example.yaml:23 weights_retries: 3 # base/config/master.example.yaml:24 weights_freshness_seconds: 720 # base/config/master.example.yaml:25 database: # base/config/master.example.yaml:28 url: postgresql+asyncpg://platform:@postgres.platform.svc.cluster.local/platform docker: network_name: platform_challenges # base/config/master.example.yaml:31 secret_dir: /var/lib/platform/secrets # base/config/master.example.yaml:32 internal_network: true # base/config/master.example.yaml:33 broker_host: 0.0.0.0 # base/config/master.example.yaml:34 broker_port: 8082 # base/config/master.example.yaml:35 broker_url: http://platform-docker-broker:8082 # base/config/master.example.yaml:36 broker_workspace_dir: /tmp/platform-docker-broker # base/config/master.example.yaml:37 broker_allowed_images: - ghcr.io// # base/config/master.example.yaml:39 (namespace redacted) challenge_placement_constraint: node.role==manager # base/config/master.example.yaml:41 cpu_job_constraint: node.labels.platform.workload==cpu # base/config/master.example.yaml:42 gpu_job_constraint: node.labels.platform.workload==gpu # base/config/master.example.yaml:43 security: admin_token_file: /var/lib/platform/secrets/admin_token # base/config/master.example.yaml:46 observability: log_json: true # base/config/master.example.yaml:49 sentry_dsn: null # base/config/master.example.yaml:50 otel_service_name: platform-master # base/config/master.example.yaml:51 ``` Use this for `platform validator run` and `platform registry print`. The full source is `base/config/validator.example.yaml`. ```yaml theme={"dark"} network: name: platform # base/config/validator.example.yaml:2 netuid: 100 # base/config/validator.example.yaml:3 chain_endpoint: null # base/config/validator.example.yaml:4 wallet_name: default # base/config/validator.example.yaml:5 wallet_hotkey: default # base/config/validator.example.yaml:6 wallet_path: null # base/config/validator.example.yaml:7 master_uid: 0 # base/config/validator.example.yaml:8 validator: registry_url: https://chain.platform.network # base/config/validator.example.yaml:16 registry_retry_seconds: 15 # base/config/validator.example.yaml:17 weights_url: null # base/config/validator.example.yaml:18 weights_interval_seconds: 360 # base/config/validator.example.yaml:19 weights_timeout_seconds: 15.0 # base/config/validator.example.yaml:20 weights_retries: 3 # base/config/validator.example.yaml:21 weights_freshness_seconds: 720 # base/config/validator.example.yaml:22 database: # base/config/validator.example.yaml:25 url: postgresql+asyncpg://platform:@postgres.platform.svc.cluster.local/platform docker: network_name: platform_challenges # base/config/validator.example.yaml:28 secret_dir: /var/lib/platform/secrets # base/config/validator.example.yaml:29 internal_network: true # base/config/validator.example.yaml:30 broker_url: http://platform-docker-broker:8082 # base/config/validator.example.yaml:31 broker_allowed_images: - ghcr.io// # base/config/validator.example.yaml:33 (namespace redacted) observability: log_json: true # base/config/validator.example.yaml:36 sentry_dsn: null # base/config/validator.example.yaml:37 otel_service_name: platform-validator # base/config/validator.example.yaml:38 ``` Operators who cannot meet the validator infrastructure requirements can instead delegate validation power to the recommended Platform validator hotkey via Bittensor child-hotkey / stake-weight, rather than running the validator stack themselves (`base/config/validator.example.yaml:10-13`). Every key, type, and default for these files. ## Related What the master configuration drives at runtime. The validator-side configuration walkthrough. The load order, environment overrides, and production policy. # Configuration Source: https://docs.joinbase.ai/reference/configuration How Platform loads configuration: YAML files, environment overrides, settings sections, and the production policy. Every `platform` command loads its configuration the same way: read a YAML file, overlay environment variables, then validate the result against the settings model and production policy (`base/src/platform_network/config/loader.py:37-47`). ## Load order and precedence `load_settings(path)` builds the configuration in three steps (`base/src/platform_network/config/loader.py:37-47`): The file at `--config` is parsed as YAML and must contain a mapping; a missing file raises `FileNotFoundError` and a non-mapping raises `ValueError` (`base/src/platform_network/config/loader.py:39-46`). Any `PLATFORM_`-prefixed environment variable is applied on top of the file values (`base/src/platform_network/config/loader.py:28-34`). The merged data is validated into the `Settings` model, which also runs the production policy (`base/src/platform_network/config/loader.py:47`, `base/src/platform_network/config/settings.py:128-131`). Because environment variables are applied after the file is read, **environment variables override file values** (`base/src/platform_network/config/loader.py:46-47`). ## Environment variable overrides Environment overrides use the `PLATFORM_` prefix and map nested settings with a double underscore (`__`) separator. The remainder of the key is lowercased and split into a nested path (`base/src/platform_network/config/loader.py:28-33`). | Setting | Environment variable | | ------------------------ | ---------------------------------- | | `master.proxy_port` | `PLATFORM_MASTER__PROXY_PORT` | | `database.url` | `PLATFORM_DATABASE__URL` | | `validator.registry_url` | `PLATFORM_VALIDATOR__REGISTRY_URL` | Values that begin with `[` or `{` are parsed as YAML, so lists and mappings can be supplied inline (`base/src/platform_network/config/loader.py:12-18`): ```bash theme={"dark"} export PLATFORM_MASTER__PROXY_PORT=9000 export PLATFORM_MASTER__UPLOAD_EXTRA_REGISTERED_HOTKEYS='["5Fh...","5Gd..."]' ``` ## Settings sections The top-level `Settings` model groups configuration into sections (`base/src/platform_network/config/settings.py:118-126`): | Section | Purpose | Source | | --------------- | ---------------------------------------------------------------------------- | -------------------------------------------------- | | `environment` | Deployment environment; drives production policy. Defaults to `development`. | `base/src/platform_network/config/settings.py:119` | | `network` | Chain, wallet, and subnet identity. | `base/src/platform_network/config/settings.py:120` | | `master` | Master proxy, epoch, registry, and upload settings. | `base/src/platform_network/config/settings.py:121` | | `validator` | Validator registry and weights settings. | `base/src/platform_network/config/settings.py:122` | | `database` | Database connection URL. | `base/src/platform_network/config/settings.py:123` | | `docker` | Swarm broker, networking, and job placement. | `base/src/platform_network/config/settings.py:124` | | `security` | Admin token source. | `base/src/platform_network/config/settings.py:125` | | `observability` | Logging, Sentry, and OpenTelemetry. | `base/src/platform_network/config/settings.py:126` | See the [Settings reference](/reference/settings) for every key, type, and default. ## Production policy When `environment` is `prod`, `production`, or `staging`, a stricter policy is enforced at load time (`base/src/platform_network/config/policy.py:6`, `:30-35`). The policy runs automatically through the settings validator on every load (`base/src/platform_network/config/settings.py:128-131`), raising `ProductionPolicyError` on any violation (`base/src/platform_network/config/policy.py:26-27`). | Rule | Requirement | Source | | ---------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------- | | Database | Must be an external PostgreSQL URL; `sqlite` is rejected. | `base/src/platform_network/config/policy.py:42-50` | | Image allowlist | Prefixes must include a registry with a dot and a namespace; broad roots and wildcards are rejected. | `base/src/platform_network/config/policy.py:53-67` | | Image references | Must include a tag (semver or `latest`) **and** a `sha256:` digest. | `base/src/platform_network/config/policy.py:70-85` | | TLS | `verify_tls` may not be set to `false`. | `base/src/platform_network/config/policy.py:88-94` | In non-production environments these checks are skipped — each validator returns early when `production` is false (`base/src/platform_network/config/policy.py:43-44`, `:54-55`, `:71-72`). Set `environment: production` only once your database is PostgreSQL and your image allowlist uses fully-qualified registry prefixes (for example `ghcr.io//`). Otherwise the command fails fast at startup (`base/src/platform_network/config/policy.py:97-102`). ## Related How the admin token and production policy protect the master. Every key, type, and default these files accept. Copy-ready master and validator configuration files. # Settings reference Source: https://docs.joinbase.ai/reference/settings Every Platform configuration key, with its type, default, and source. Grouped by settings section. This page documents every key in the `Settings` model (`base/src/platform_network/config/settings.py`). All keys live under a section (for example `master.proxy_port`). See [Configuration](/reference/configuration) for how files and environment variables are merged. ## Top level | Key | Type | Default | Source | | ------------- | ------ | ------------- | -------------------------------------------------- | | `environment` | string | `development` | `base/src/platform_network/config/settings.py:119` | The `environment` value drives the [production policy](/reference/configuration#production-policy) when set to `prod`, `production`, or `staging`. ## network Chain, wallet, and subnet identity (`base/src/platform_network/config/settings.py:10-17`). | Key | Type | Default | Source | | ------------------------ | -------------- | ---------- | ------------------------------------------------- | | `network.name` | string | `platform` | `base/src/platform_network/config/settings.py:11` | | `network.netuid` | integer | `100` | `base/src/platform_network/config/settings.py:12` | | `network.chain_endpoint` | string or null | `null` | `base/src/platform_network/config/settings.py:13` | | `network.wallet_name` | string | `default` | `base/src/platform_network/config/settings.py:14` | | `network.wallet_hotkey` | string | `default` | `base/src/platform_network/config/settings.py:15` | | `network.wallet_path` | string or null | `null` | `base/src/platform_network/config/settings.py:16` | | `network.master_uid` | integer | `0` | `base/src/platform_network/config/settings.py:17` | ## master Master proxy, epoch scheduling, registry, and upload acceptance (`base/src/platform_network/config/settings.py:20-38`). | Key | Type | Default | Source | | ----------------------------------------- | -------------- | --------------------------------- | ------------------------------------------------- | | `master.registry_url` | string | `https://chain.platform.network` | `base/src/platform_network/config/settings.py:21` | | `master.proxy_host` | string | `0.0.0.0` | `base/src/platform_network/config/settings.py:26` | | `master.proxy_port` | integer | `8081` | `base/src/platform_network/config/settings.py:27` | | `master.epoch_interval_seconds` | integer | `360` | `base/src/platform_network/config/settings.py:28` | | `master.metagraph_cache_ttl_seconds` | integer | `300` | `base/src/platform_network/config/settings.py:29` | | `master.challenge_timeout_seconds` | float | `10.0` | `base/src/platform_network/config/settings.py:30` | | `master.challenge_retries` | integer | `3` | `base/src/platform_network/config/settings.py:31` | | `master.registry_state_file` | string | `/var/lib/platform/registry.json` | `base/src/platform_network/config/settings.py:32` | | `master.upload_signature_ttl_seconds` | integer | `300` | `base/src/platform_network/config/settings.py:33` | | `master.upload_nonce_ttl_seconds` | integer | `86400` | `base/src/platform_network/config/settings.py:34` | | `master.upload_max_body_bytes` | integer | `7500000` | `base/src/platform_network/config/settings.py:35` | | `master.upload_require_registered_hotkey` | boolean | `true` | `base/src/platform_network/config/settings.py:36` | | `master.upload_extra_registered_hotkeys` | list of string | `[]` | `base/src/platform_network/config/settings.py:38` | `master.admin_host` and `master.admin_port` exist only for back-compat and are ignored — the admin and registry API is served by the proxy on `proxy_port` as a single public API (`base/src/platform_network/config/settings.py:22-25`). ## validator Validator registry polling and weight submission (`base/src/platform_network/config/settings.py:41-52`). | Key | Type | Default | Source | | ------------------------------------- | -------------- | -------------------------------- | ------------------------------------------------- | | `validator.registry_url` | string | `https://chain.platform.network` | `base/src/platform_network/config/settings.py:42` | | `validator.registry_retry_seconds` | integer | `15` | `base/src/platform_network/config/settings.py:43` | | `validator.weights_url` | string or null | `null` | `base/src/platform_network/config/settings.py:44` | | `validator.weights_interval_seconds` | integer | `360` | `base/src/platform_network/config/settings.py:45` | | `validator.weights_timeout_seconds` | float | `15.0` | `base/src/platform_network/config/settings.py:46` | | `validator.weights_retries` | integer | `3` | `base/src/platform_network/config/settings.py:47` | | `validator.weights_freshness_seconds` | integer | `720` | `base/src/platform_network/config/settings.py:48` | The effective weights endpoint is `weights_url` if set, otherwise `registry_url` (`base/src/platform_network/config/settings.py:50-52`). ## database | Key | Type | Default | Source | | -------------- | ------ | ------------------------------------------------------------------------------------- | ------------------------------------------------- | | `database.url` | string | `postgresql+asyncpg://platform:platform@postgres.platform.svc.cluster.local/platform` | `base/src/platform_network/config/settings.py:56` | In production the URL must point to an external PostgreSQL instance; `sqlite` URLs are rejected (`base/src/platform_network/config/policy.py:42-50`). ## docker Swarm broker, challenge networking, and job placement (`base/src/platform_network/config/settings.py:59-104`). | Key | Type | Default | Source | | -------------------------------------------- | --------------------- | ------------------------------------ | ---------------------------------------------------- | | `docker.network_name` | string | `platform_challenges` | `base/src/platform_network/config/settings.py:60` | | `docker.secret_dir` | string | `/var/lib/platform/secrets` | `base/src/platform_network/config/settings.py:61` | | `docker.internal_network` | boolean | `true` | `base/src/platform_network/config/settings.py:62` | | `docker.broker_host` | string | `0.0.0.0` | `base/src/platform_network/config/settings.py:63` | | `docker.broker_port` | integer | `8082` | `base/src/platform_network/config/settings.py:64` | | `docker.broker_url` | string | `http://platform-docker-broker:8082` | `base/src/platform_network/config/settings.py:65` | | `docker.broker_workspace_dir` | string | `/tmp/platform-docker-broker` | `base/src/platform_network/config/settings.py:66` | | `docker.broker_allowed_images` | list of string | `["ghcr.io//"]` | `base/src/platform_network/config/settings.py:67-69` | | `docker.allow_privileged` | boolean | `false` | `base/src/platform_network/config/settings.py:70` | | `docker.broker_privileged_slugs` | list of string | `[]` | `base/src/platform_network/config/settings.py:71` | | `docker.broker_node_role` | `manager` or `worker` | `manager` | `base/src/platform_network/config/settings.py:72` | | `docker.broker_allow_privileged_escape` | boolean | `false` | `base/src/platform_network/config/settings.py:73` | | `docker.broker_docker_socket_slugs` | list of string | `[]` | `base/src/platform_network/config/settings.py:81` | | `docker.broker_docker_socket_path` | string | `/var/run/docker.sock` | `base/src/platform_network/config/settings.py:82` | | `docker.broker_eval_readonly_mounts` | list of string | `[]` | `base/src/platform_network/config/settings.py:89` | | `docker.broker_eval_readonly_mounts_by_slug` | map of string to list | `{}` | `base/src/platform_network/config/settings.py:97-99` | | `docker.challenge_placement_constraint` | string or null | `node.role==manager` | `base/src/platform_network/config/settings.py:102` | | `docker.cpu_job_constraint` | string or null | `node.labels.platform.workload==cpu` | `base/src/platform_network/config/settings.py:103` | | `docker.gpu_job_constraint` | string or null | `node.labels.platform.workload==gpu` | `base/src/platform_network/config/settings.py:104` | The `broker_allowed_images` default is shown with the registry namespace redacted as ``. Set it to your own fully-qualified registry prefix (for example `ghcr.io//`); broad prefixes are rejected under the production policy (`base/src/platform_network/config/policy.py:53-67`). ## security Admin token used to authorize admin API calls (`base/src/platform_network/config/settings.py:107-109`). | Key | Type | Default | Source | | --------------------------- | -------------- | ------- | -------------------------------------------------- | | `security.admin_token` | string or null | `null` | `base/src/platform_network/config/settings.py:108` | | `security.admin_token_file` | string or null | `null` | `base/src/platform_network/config/settings.py:109` | Provide the token inline with `admin_token`, or point `admin_token_file` at a file containing it. ## observability Logging and telemetry (`base/src/platform_network/config/settings.py:112-115`). | Key | Type | Default | Source | | --------------------------------- | -------------- | ---------- | -------------------------------------------------- | | `observability.log_json` | boolean | `true` | `base/src/platform_network/config/settings.py:113` | | `observability.sentry_dsn` | string or null | `null` | `base/src/platform_network/config/settings.py:114` | | `observability.otel_service_name` | string | `platform` | `base/src/platform_network/config/settings.py:115` | ## Related What each settings section actually configures. These keys shown in complete, annotated files. # Upload protocol Source: https://docs.joinbase.ai/reference/upload-protocol The signed submission protocol miners use to upload to a challenge through the subnet master proxy. Miners upload submissions to a challenge through a signed proxy route on the subnet master. The master verifies the miner's signature, reserves a one-time nonce, then bridges the body to the challenge with verified-identity headers. ## `POST /v1/challenges/{challenge_name}/submissions` Uploads a signed submission for the named challenge (`app_proxy.py:510-512`, `app_proxy.py:424-484`). The challenge must be registered and `active`; otherwise the master returns `404` (`app_proxy.py:425`, `app_proxy.py:243-254`). ## Required signature headers Every upload MUST carry these four headers; a missing header is rejected with `401` (`miner_auth.py:159-162`, `miner_auth.py:230-234`): | Header | Meaning | Source | | ------------- | ------------------------------------ | ------------------- | | `X-Hotkey` | The miner's SS58 hotkey address | `miner_auth.py:159` | | `X-Signature` | Signature over the canonical message | `miner_auth.py:160` | | `X-Nonce` | Unique per-request nonce | `miner_auth.py:161` | | `X-Timestamp` | Unix timestamp (integer seconds) | `miner_auth.py:162` | An optional `X-Submission-Filename` header is forwarded to the challenge when present (`app_proxy.py:463-465`). ## Canonical message The signature is computed over this exact byte string (`canonical_upload_message`, `miner_auth.py:96-111`): ``` platform-upload-v1:{netuid}:{challenge_slug}:{METHOD}:{path}:{hotkey}:{nonce}:{timestamp}:{body_hash} ``` Where (`miner_auth.py:107-111`, `miner_auth.py:170-180`): * `netuid` — the subnet id, `100` for BASE (`config/settings.py:12`). * `challenge_slug` — the active challenge's slug (`app_proxy.py:438`). * `METHOD` — the HTTP method, upper-cased (`miner_auth.py:109`). * `path` — the request path being signed, i.e. the public submissions path (`app_proxy.py:435-436`). * `hotkey`, `nonce`, `timestamp` — the values from the matching headers (`miner_auth.py:171-178`). * `body_hash` — `sha256(body).hexdigest()` of the raw request body (`miner_auth.py:170`). The signature is verified against the hotkey as a substrate SS58 keypair; a `0x`-prefixed hex signature is hex-decoded before verification (`verify_substrate_signature`, `miner_auth.py:114-121`; `_decode_signature`, `miner_auth.py:222-227`). ## Verification rules The master enforces, in order: 1. **Body size** — bodies larger than the configured limit (default `2_000_000` bytes) return `413` (`app_proxy.py:274`, `app_proxy.py:427-431`). 2. **Timestamp freshness** — if `abs(now - timestamp)` exceeds the TTL (default `300` seconds) the signature is rejected as stale (`app_proxy.py:272`, `miner_auth.py:163-169`). 3. **Signature** — an invalid signature is rejected (`miner_auth.py:181-182`). 4. **Hotkey registration** — by default the hotkey must be registered in the metagraph; an unknown hotkey is rejected and UID `0` is blocked (`app_proxy.py:275`, `miner_auth.py:200-219`). 5. **Nonce replay** — the nonce is reserved once; a repeat returns `409` (`miner_auth.py:184-191`, `app_proxy.py:440-441`). ## Response and error codes | Status | When | Source | | ------ | ------------------------------------------------------------- | ---------------------------------------------- | | `401` | Missing/invalid signature, stale timestamp, or unknown hotkey | `app_proxy.py:442-443` | | `409` | Nonce already used (replay) | `app_proxy.py:440-441` | | `413` | Submission larger than the body limit | `app_proxy.py:427-431` | | `404` | Challenge not registered or not active | `app_proxy.py:243-254` | | `502` | Challenge unreachable, or challenge token unavailable | `app_proxy.py:446-449`, `app_proxy.py:475-478` | On success, the master bridges the body to the challenge and returns the challenge's response (status, body, and content type) unchanged (`app_proxy.py:479-484`). The response body shape is owned by the challenge and is not defined in the master. ## What the master forwards to the challenge After verification, the master POSTs the body to the challenge's internal bridge route `/internal/v1/bridge/submissions` (`app_proxy.py:470`) with these headers (`app_proxy.py:450-465`): | Header | Value | | ---------------------------- | --------------------------------------------------------- | | `Authorization` | `Bearer ` (`app_proxy.py:451`) | | `X-Platform-Challenge-Slug` | the challenge slug (`app_proxy.py:452`) | | `X-Platform-Verified-Hotkey` | the verified hotkey (`app_proxy.py:453`) | | `X-Platform-Verified-Nonce` | the verified nonce (`app_proxy.py:454`) | | `X-Platform-Request-Hash` | the body hash (`app_proxy.py:455`) | | `X-Platform-Verified-Uid` | the resolved UID, when available (`app_proxy.py:461-462`) | ## Example ```bash theme={"dark"} # Construct the canonical message, sign it with your hotkey, then upload. # netuid=100, METHOD=POST, path=/v1/challenges/agent-challenge/submissions BODY_HASH=$(sha256sum submission.bin | cut -d' ' -f1) curl -s -X POST "$MASTER_URL/v1/challenges/agent-challenge/submissions" \ -H "X-Hotkey: $HOTKEY_SS58" \ -H "X-Signature: $SIGNATURE_HEX" \ -H "X-Nonce: $NONCE" \ -H "X-Timestamp: $TIMESTAMP" \ -H "Content-Type: application/octet-stream" \ --data-binary @submission.bin ``` The `path` you sign must match the request path exactly (`/v1/challenges/{challenge_name}/submissions`), and `body_hash` must be the SHA-256 hex digest of the exact bytes you send. ## Related How signed uploads are verified at the proxy. The miner-side guide to building and signing a submission. The proxy routes that sit alongside this upload. # Weights schema Source: https://docs.joinbase.ai/reference/weights-schema Field-level reference for the weight vector models used by the subnet master and challenges. This page is the field-level reference for the weight models defined in `schemas/weights.py`. For the public read that returns the aggregated vector, see [Weights API](/reference/api-weights). ## `MasterWeightsResponse` The aggregated, chain-ready weight vector returned by `GET /v1/weights/latest` (`schemas/weights.py:33-42`). | Field | Type | Default | Source | | ---------------------- | --------------------------------- | ------- | ----------------------- | | `netuid` | integer | — | `schemas/weights.py:34` | | `chain_endpoint` | string | — | `schemas/weights.py:35` | | `uids` | array of integer | — | `schemas/weights.py:36` | | `weights` | array of number | — | `schemas/weights.py:37` | | `hotkey_weights` | object (string → number) | `{}` | `schemas/weights.py:38` | | `computed_at` | datetime | — | `schemas/weights.py:39` | | `expires_at` | datetime | — | `schemas/weights.py:40` | | `source_challenges` | array of `ChallengeWeightsResult` | — | `schemas/weights.py:41` | | `metagraph_updated_at` | datetime | — | `schemas/weights.py:42` | `uids` and `weights` are parallel arrays — the weight at index `i` applies to the UID at index `i`. `expires_at` is validated to be in the future when the model is constructed; a non-future value raises a validation error (`schemas/weights.py:44-48`). ## `ChallengeWeightsResult` A single challenge's contribution inside `source_challenges` (`schemas/weights.py:19-24`). | Field | Type | Default | Source | | ------------------ | ------------------------ | ------- | ----------------------- | | `slug` | string | — | `schemas/weights.py:20` | | `emission_percent` | number | — | `schemas/weights.py:21` | | `weights` | object (string → number) | `{}` | `schemas/weights.py:22` | | `ok` | boolean | `true` | `schemas/weights.py:23` | | `error` | string or null | `null` | `schemas/weights.py:24` | When a challenge collection fails, `ok` is `false` and `error` carries the failure detail while `weights` stays empty (`challenge_client.py:54-59`). ## `ChallengeWeightsResponse` The shape a challenge returns from its internal `get_weights` endpoint, which the master collects per challenge (`schemas/weights.py:11-16`, `challenge_client.py:31`, `challenge_client.py:43`). | Field | Type | Default | Source | | ---------------- | ------------------------ | --------- | ----------------------- | | `challenge_slug` | string | — | `schemas/weights.py:12` | | `epoch` | integer or null | `null` | `schemas/weights.py:13` | | `weights` | object (string → number) | `{}` | `schemas/weights.py:14` | | `metadata` | object | `{}` | `schemas/weights.py:15` | | `computed_at` | datetime | now (UTC) | `schemas/weights.py:16` | ## `FinalWeights` The internal vector model pairing UIDs with weights and per-hotkey weights (`schemas/weights.py:27-30`). | Field | Type | Default | Source | | ---------------- | ------------------------ | ------- | ----------------------- | | `uids` | array of integer | — | `schemas/weights.py:28` | | `weights` | array of number | — | `schemas/weights.py:29` | | `hotkey_weights` | object (string → number) | `{}` | `schemas/weights.py:30` | ## Freshness constant `MASTER_WEIGHTS_FRESHNESS_SECONDS = 720` defines the freshness window for the master weights (`schemas/weights.py:8`). ## Related How these models are produced each epoch. The public read that returns these models. # Configuration Source: https://docs.joinbase.ai/validators/configuration Every validator config field, its default, and where it comes from. The validator reads a YAML config. Start from `config/validator.example.yaml` and edit the network and validator blocks. Defaults below come from `ValidatorSettings` and `NetworkSettings` (`src/platform_network/config/settings.py:10-52`). ## Network block The network block sets identity and chain connection (`config/validator.example.yaml:1-8`, `src/platform_network/config/settings.py:10-17`): | Key | Default | Source | | ---------------- | ---------- | -------------------------------------------- | | `name` | `platform` | `settings.py:11`, `validator.example.yaml:2` | | `netuid` | `100` | `settings.py:12`, `validator.example.yaml:3` | | `chain_endpoint` | `null` | `settings.py:13`, `validator.example.yaml:4` | | `wallet_name` | `default` | `settings.py:14`, `validator.example.yaml:5` | | `wallet_hotkey` | `default` | `settings.py:15`, `validator.example.yaml:6` | | `wallet_path` | `null` | `settings.py:16`, `validator.example.yaml:7` | | `master_uid` | `0` | `settings.py:17`, `validator.example.yaml:8` | ## Validator block The validator block controls the registry reconcile loop and the weight submit loop (`config/validator.example.yaml:15-22`, `src/platform_network/config/settings.py:41-52`): | Key | Default | Purpose | Source | | --------------------------- | -------------------------------- | ---------------------------- | --------------------------------------------- | | `registry_url` | `https://chain.platform.network` | Registry the validator reads | `settings.py:42`, `validator.example.yaml:16` | | `registry_retry_seconds` | `15` | Reconcile retry interval | `settings.py:43`, `validator.example.yaml:17` | | `weights_url` | `null` | Weights source override | `settings.py:44`, `validator.example.yaml:18` | | `weights_interval_seconds` | `360` | Submit loop interval | `settings.py:45`, `validator.example.yaml:19` | | `weights_timeout_seconds` | `15.0` | Weights fetch timeout | `settings.py:46`, `validator.example.yaml:20` | | `weights_retries` | `3` | Weights fetch retries | `settings.py:47`, `validator.example.yaml:21` | | `weights_freshness_seconds` | `720` | Max vector age before skip | `settings.py:48`, `validator.example.yaml:22` | ### Weights URL resolution `weights_url` defaults to `null`. When it is null, the validator fetches weights from `registry_url`. The `resolved_weights_url` property returns `weights_url` when set, otherwise `registry_url` (`src/platform_network/config/settings.py:50-52`). ## Database block The validator config includes a database URL using the asyncpg driver (`config/validator.example.yaml:24-25`): ```yaml theme={"dark"} database: url: postgresql+asyncpg://platform:@postgres.platform.svc.cluster.local/platform ``` ## Observability block Logging defaults to JSON output (`config/validator.example.yaml:35-38`, `src/platform_network/config/settings.py:112-115`): | Key | Default | Source | | ------------------- | -------------------- | ---------------------------------------------- | | `log_json` | `true` | `settings.py:113`, `validator.example.yaml:36` | | `sentry_dsn` | `null` | `settings.py:114`, `validator.example.yaml:37` | | `otel_service_name` | `platform-validator` | `validator.example.yaml:38` | ## Delegation instead of self-hosting If running the validator stack is too demanding, you can delegate validation power with Bittensor CHK / stake-weight check to the recommended validator hotkey, rather than running this stack yourself (`config/validator.example.yaml:10-13`). ## Next steps Apply the config and start the runtime. How the cadence values drive submissions. # Install the validator Source: https://docs.joinbase.ai/validators/installation Install the platform CLI with the bittensor extra needed to submit weights. The validator ships as part of the `platform-network` package. Installing it gives you the `platform` CLI (`pyproject.toml:43`). ## Requirements * Python **3.12 or newer** (`pyproject.toml:5`). * A Bittensor wallet (coldkey + hotkey) for signing on-chain submissions. ## Install The validator extra installs `bittensor>=9`, which is required to submit weights (`pyproject.toml:40`). ```bash theme={"dark"} pip install 'platform-network[validator]' ``` Submitting weights needs the bittensor dependency. If it is missing, the runtime raises a clear error telling you to install the extra (`src/platform_network/bittensor/factory.py:27-28`): ```bash theme={"dark"} pip install 'platform-network[bittensor]' ``` Check that the `platform` command resolves. ```bash theme={"dark"} platform --help ``` ## Validator node install (Docker Swarm) For the submit-only deployment used on the validator node, the supervisor venv provides the Python interpreter, and the wallet lives under `/var/lib/platform/wallets//hotkeys/` (`deploy/swarm/submitter/run_submitter.py:24-28`). See [The submitter](/validators/submitter) for that path. The validator node holds the hotkey. Keep the private key off shared hosts and out of logs. The submitter logs only the public hotkey SS58 address (`deploy/swarm/submitter/run_submitter.py:65-87`). ## Next steps Configure and run in five steps. Fill out your validator config. # Monitoring & logs Source: https://docs.joinbase.ai/validators/monitoring Read validator log output and confirm weights are being submitted. The validator logs through the shared logging setup. Logging is configured at startup and defaults to structured JSON (`src/platform_network/observability/logging.py:21-28`, `config/validator.example.yaml:36`). ## Log format With `log_json: true` (the default), each record is emitted as a JSON object with a timestamp, level, logger name, and message (`src/platform_network/observability/logging.py:8-18`, `src/platform_network/config/settings.py:113`). Set `log_json: false` for a plain text formatter (`src/platform_network/observability/logging.py:24-26`). ## What to watch in the runtime The full runtime logs at these points: * A registry sync failure is logged and retried on the next pass (`validator/normal_runner.py:114`). * A weights fetch failure is logged (`validator/normal_runner.py:75`). * A skipped submission is logged with the reason, for example a stale or expired payload (`validator/normal_runner.py:80`). * A submission rejected by the subtensor is logged (`validator/normal_runner.py:89`). * A submission that is not configured logs a warning (`validator/normal_runner.py:70`). ## What to watch in the submitter The submit-only process logs each iteration in detail (`deploy/swarm/submitter/run_submitter.py`): Logs the weights URL, netuid, interval, freshness, and the public hotkey SS58 address (`deploy/swarm/submitter/run_submitter.py:65-87`). Logs `netuid`, `n_uids`, `n_weights`, and `computed_at` for the fetched vector (`deploy/swarm/submitter/run_submitter.py:117-123`). On success, logs `netuid` and the number of weights submitted on-chain (`deploy/swarm/submitter/run_submitter.py:140-144`). ## Confirm connectivity Print the live registry the validator reads to confirm it can reach the master (`src/platform_network/cli_app/main.py:943-948`): ```bash theme={"dark"} platform registry print --config config/validator.yaml ``` ## Service logs When the submitter runs as a systemd unit, read its logs with `journalctl`. The unit is `platform-submitter.service` (`deploy/swarm/submitter/platform-submitter.service:29`): ```bash theme={"dark"} journalctl -u platform-submitter.service -f ``` ## Docker log rotation On the validator node, the Docker daemon config caps log files at `50m` with `5` files and enables `live-restore` (`deploy/swarm/daemon.validator.json:1-8`). ## Next steps Map a log line to a fix. Why a submission may be skipped. # Validating on BASE Source: https://docs.joinbase.ai/validators/overview What a validator does on the BASE subnet and how the pieces fit together. A BASE validator operates **subnet infrastructure** and sets weights on netuid **100**. BASE is not the miner application surface. Miners compete inside [challenges](/challenges/overview). The master computes the final weight vector from each challenge's raw scores; the validator fetches that vector and submits it on-chain with its hotkey. Challenge-specific operator tasks (for example Agent Challenge dual allowlists and RA-TLS key release, or PRISM GPU workers) are **challenge operator** docs under Challenges, not a substitute for this validator pack. ## What a validator runs The `platform validator run` command starts two concurrent loops (`src/platform_network/cli_app/main.py:821-831`): Fetches the challenge registry and starts the active challenge containers. See `validator/normal_runner.py:45-62`. Fetches the master's latest weight vector and submits it on-chain every `weights_interval_seconds`. See `validator/normal_runner.py:64-91`. ## How weights flow 1. The master computes the final UID weight vector per epoch (`src/platform_network/cli_app/main.py:784-818`). 2. The validator fetches that vector over HTTP from `GET /v1/weights/latest` (`validator/weights_client.py:18-30`). 3. The validator validates freshness and shape, then calls `set_weights` on-chain (`validator/normal_runner.py:64-107`, `bittensor/weight_setter.py:27-44`). Read [Weight setting](/validators/weight-setting) for the cadence and the validation rules. ## Two ways to validate Run `platform validator run` to reconcile challenges and submit weights from one process (`src/platform_network/cli_app/main.py:834-854`). Start at the [Quickstart](/validators/quickstart). If the infrastructure requirements are too high, you can use Bittensor CHK / stake-weight check to give validation power to the recommended validator hotkey instead of running the stack yourself (`config/validator.example.yaml:10-13`). ## Identity and keys The validator signs with its [**hotkey**](/concepts/glossary#hotkey); ownership stays with the [**coldkey**](/concepts/glossary#coldkey). The wallet defaults are `wallet_name: default` and `wallet_hotkey: default` (`config/validator.example.yaml:5-6`), and `master_uid` defaults to `0` (`config/validator.example.yaml:8`). ## Next steps Get a validator submitting weights fast. Install the package and its bittensor extra. Start the runtime and keep it healthy. Every validator setting and its default. # Validator quickstart Source: https://docs.joinbase.ai/validators/quickstart Install, configure, and start a BASE validator from the CLI. This is the fastest path to a running validator that submits weights on-chain for netuid **100** (`src/platform_network/config/settings.py:12`). ## Prerequisites * Python **3.12 or newer** (`pyproject.toml:5`). * A Bittensor wallet (coldkey + hotkey) registered on netuid 100. * The `platform` CLI installed with its bittensor extra (next step). ## Get started Install the package with the validator extra, which pulls in `bittensor>=9` (`pyproject.toml:40`). ```bash theme={"dark"} pip install 'platform-network[validator]' ``` The install provides the `platform` command (`pyproject.toml:43`). Copy the example validator config and edit the wallet and network fields. ```bash theme={"dark"} cp config/validator.example.yaml config/validator.yaml ``` The example sets `netuid: 100` and the validator loop defaults (`config/validator.example.yaml:1-22`). See [Configuration](/validators/configuration) for every field. `registry_url` defaults to `https://chain.platform.network` (`config/validator.example.yaml:16`). When `weights_url` is left null, the validator fetches weights from the same `registry_url` (`src/platform_network/config/settings.py:50-52`). Start the runtime. It reconciles challenges and submits weights on-chain (`src/platform_network/cli_app/main.py:834-854`). ```bash theme={"dark"} platform validator run --config config/validator.yaml ``` The default config path is `config/validator.example.yaml` (`src/platform_network/cli_app/main.py:835`). The submit loop runs every `weights_interval_seconds`, which defaults to **360 seconds** (`config/validator.example.yaml:19`, `src/platform_network/config/settings.py:45`). Watch the logs for weight submission. See [Monitoring](/validators/monitoring). ## Verify the registry connection Print the live registry the validator reads, to confirm connectivity (`src/platform_network/cli_app/main.py:943-948`): ```bash theme={"dark"} platform registry print --config config/validator.yaml ``` If the validator infrastructure requirements are too high, you can delegate validation power with Bittensor CHK / stake-weight check instead of running this stack (`config/validator.example.yaml:10-13`). ## Next steps Runtime details and the two concurrent loops. The cadence and the on-chain submit path. # Running a validator Source: https://docs.joinbase.ai/validators/running Start the validator runtime and understand the two loops it runs. `platform validator run` starts the normal validator runtime (`src/platform_network/cli_app/main.py:834-854`). It runs two coroutines concurrently with `asyncio.gather` (`src/platform_network/cli_app/main.py:821-831`). ## Start the runtime ```bash theme={"dark"} platform validator run --config config/validator.yaml ``` The config defaults to `config/validator.example.yaml` when `--config` is omitted (`src/platform_network/cli_app/main.py:835`). Startup constructs the bittensor submit runtime (subtensor + wallet + weight setter) from your config (`src/platform_network/cli_app/main.py:838`, `src/platform_network/bittensor/factory.py:62-76`). ## The two loops `run_forever` calls `run_once` and then sleeps `retry_seconds` between passes (`validator/normal_runner.py:109-115`). Each pass fetches the registry and starts the active challenges (`validator/normal_runner.py:45-62`). `run_epoch_loop` calls `submit_latest_weights` every `weights_interval_seconds` (`src/platform_network/cli_app/main.py:828-831`, `bittensor/validator_loop.py:10-18`). ## Reconcile cadence The reconcile loop retries on the `registry_retry_seconds` interval, which defaults to **15 seconds** (`config/validator.example.yaml:17`, `src/platform_network/config/settings.py:43`). It maps to the runner's `retry_seconds` (`validator/normal_runner.py:31`). On any error it logs and retries on the next interval (`validator/normal_runner.py:109-115`). ## Submit cadence The submit loop fetches and submits weights every `weights_interval_seconds`, which defaults to **360 seconds** (`config/validator.example.yaml:19`, `src/platform_network/config/settings.py:45`). For the full submit path, read [Weight setting](/validators/weight-setting). ## Where weights come from The validator fetches the latest vector from the master over HTTP at `GET /v1/weights/latest` (`validator/weights_client.py:18-30`). The URL is `resolved_weights_url`, which is `weights_url` when set, otherwise `registry_url` (`src/platform_network/config/settings.py:50-52`). The fetch uses `weights_timeout_seconds` (default 15.0) and retries `weights_retries` times (default 3) (`config/validator.example.yaml:20-21`). ## Submit-only alternative On the Docker Swarm validator node, a trimmed submit-only process replaces the full runtime and runs only the submit loop, with no challenge orchestration and no database connection (`deploy/swarm/submitter/run_submitter.py:1-17`). See [The submitter](/validators/submitter). ## Next steps Read the runtime's log output. Fix common failures. # The submitter Source: https://docs.joinbase.ai/validators/submitter The submit-only on-chain weight submitter used on the validator node. The submitter is a trimmed, submit-only process that replaces the full `platform validator run` deployment on the validator node (`deploy/swarm/submitter/run_submitter.py:1-7`). It does one thing: every `weights_interval_seconds` it fetches the master's final weight vector over HTTP and submits it on-chain with the validator hotkey (`deploy/swarm/submitter/run_submitter.py:4-7`). ## What it deliberately omits Compared to the full validator runtime, the submitter drops two surfaces (`deploy/swarm/submitter/run_submitter.py:8-17`): It keeps only the submit loop. The registry reconcile loop that launches challenge containers is dropped, because that belongs to the master (`deploy/swarm/submitter/run_submitter.py:8-16`). The submit path never opens the control-plane database (`deploy/swarm/submitter/run_submitter.py:17`). The submit wiring mirrors production exactly: the same settings loader, submit runtime, `WeightsClient`, and `WeightSetter`, and the same interval source (`deploy/swarm/submitter/run_submitter.py:19-22`). ## Run it The `--config` argument is required and points at the submitter YAML (`deploy/swarm/submitter/run_submitter.py:57-62`). ```bash theme={"dark"} python run_submitter.py --config /etc/platform/submitter.yaml ``` At startup it logs the submit target and the public hotkey SS58 address, so you can confirm the identity before go-live. It never logs the private key (`deploy/swarm/submitter/run_submitter.py:30-32,65-87`). ## The submitter config The submitter config is the minimal config the settings loader accepts for the submit-only path, and it carries no credentials (`deploy/swarm/submitter/submitter.yaml:6-8`): ```yaml theme={"dark"} network: name: platform netuid: 100 chain_endpoint: '' wallet_name: platform-validator wallet_hotkey: validator wallet_path: /var/lib/platform/wallets master_uid: 0 validator: weights_url: https://chain.platform.network weights_interval_seconds: 360 weights_timeout_seconds: 15.0 weights_retries: 3 weights_freshness_seconds: 720 ``` Source: `deploy/swarm/submitter/submitter.yaml:27-47`. The `database` and `docker` blocks are intentionally omitted because the submit path never uses them (`deploy/swarm/submitter/submitter.yaml:10-19`). ## Run as a service The validator node runs the submitter as a systemd service (`deploy/swarm/submitter/platform-submitter.service`). The install paths are fixed (`deploy/swarm/submitter/platform-submitter.service:8-14`): * script: `/var/lib/platform/submitter/run_submitter.py` * config: `/etc/platform/submitter.yaml` * wallet: `/var/lib/platform/wallets/platform-validator/hotkeys/validator` The unit starts the interpreter against the config and restarts on failure with `Restart=always` and `RestartSec=10` (`deploy/swarm/submitter/platform-submitter.service:38-40`). ```bash theme={"dark"} systemctl enable --now platform-submitter.service ``` Source: `deploy/swarm/submitter/platform-submitter.service:21`. ## Clean shutdown On `SIGTERM` (systemd stop) or `SIGINT`, the submitter cancels at the next sleep. An in-flight `set_weights` is a single synchronous extrinsic call, so a submission already in progress completes fully and the stop never leaves a half-submitted state (`deploy/swarm/submitter/run_submitter.py:192-203`). Mutual exclusion with the k3s validator is operational, not enforced in code. Scale the k3s validator to zero and wait one weights interval before you start this unit. On rollback, stop this unit first, then scale k3s back up (`deploy/swarm/submitter/platform-submitter.service:23-26`). ## Next steps The cadence and validation the submitter applies. Read the submitter's per-iteration log lines. # Troubleshooting Source: https://docs.joinbase.ai/validators/troubleshooting Diagnose and fix common validator failures using log lines and config. Most validator issues show up as a specific log line or a skipped submission. Match the symptom below to its source and fix. ## Weights submission is skipped The validator validates every fetched vector before submitting and logs the reason when it skips (`validator/normal_runner.py:80,93-107`). The vector is older than `weights_freshness_seconds` (default 720) (`validator/normal_runner.py:99`, `src/platform_network/config/settings.py:48`). Confirm the master is computing fresh vectors, and check clock skew between the validator and master. The payload `expires_at` is in the past (`validator/normal_runner.py:97`). Confirm the master is publishing current weights. The payload netuid does not match the configured netuid (`validator/normal_runner.py:94-95`). Confirm `netuid: 100` in your config (`config/validator.example.yaml:3`). The `uids` or `weights` vector is empty, or their lengths differ (`validator/normal_runner.py:101-106`). This points at the master's computed vector, not the validator. ## Weights fetch failed A fetch failure is logged and the tick returns without submitting (`validator/normal_runner.py:74-76`). The client retries `weights_retries` times (default 3) with a `weights_timeout_seconds` timeout (default 15.0) before raising (`validator/weights_client.py:18-30`, `config/validator.example.yaml:20-21`). Confirm the validator can reach `resolved_weights_url` (`src/platform_network/config/settings.py:50-52`): ```bash theme={"dark"} platform registry print --config config/validator.yaml ``` ## Submission rejected by the subtensor A rejection is detected and logged, and the tick returns without raising (`validator/normal_runner.py:88-90`, `bittensor/weight_setter.py:7-18`). Confirm the hotkey is registered on netuid 100 and the wallet is staged correctly. ## Submission is not configured If you see a warning that weights submission is not configured, the weights client, weight setter, or netuid is missing (`validator/normal_runner.py:64-71`). The runtime builds the submit wiring from the bittensor submit runtime (`src/platform_network/cli_app/main.py:838`, `src/platform_network/bittensor/factory.py:62-76`). ## Bittensor is not installed If submission fails because bittensor is missing, install the extra (`src/platform_network/bittensor/factory.py:27-28`): ```bash theme={"dark"} pip install 'platform-network[bittensor]' ``` ## Cannot read the hotkey SS58 at startup The submitter warns when it cannot read the wallet hotkey SS58 at startup. Verify the wallet is staged at the expected path (`deploy/swarm/submitter/run_submitter.py:82-85`, `deploy/swarm/submitter/platform-submitter.service:13`). ## Submitter and k3s validator both running Mutual exclusion is operational, not enforced in code. Scale the k3s validator to zero and wait one weights interval before starting the submitter unit. On rollback, stop the submitter first, then scale k3s back up (`deploy/swarm/submitter/platform-submitter.service:23-26`). ## Next steps Find the log line behind a symptom. Check your config values. # Weight setting Source: https://docs.joinbase.ai/validators/weight-setting The validator weight-setting cadence and the on-chain submit path. The validator does not compute scores. The master computes the final UID weight vector, and the validator fetches it and submits it on-chain. This page covers the cadence and the validation the validator applies before every submission. ## The submit loop The submit loop calls `submit_latest_weights` on a fixed interval through `run_epoch_loop` (`src/platform_network/cli_app/main.py:825-831`, `bittensor/validator_loop.py:10-18`). The loop runs the callback, then sleeps the interval, then repeats (`bittensor/validator_loop.py:13-18`). ## Cadence | Cadence | Default | Source | | ------------------------- | --------- | -------------------------------------------------------------------------------- | | Validator submit interval | **360 s** | `config/validator.example.yaml:19`, `src/platform_network/config/settings.py:45` | | Master epoch interval | **360 s** | `config/master.example.yaml:14`, `src/platform_network/config/settings.py:28` | | Weights freshness ceiling | **720 s** | `config/validator.example.yaml:22`, `src/platform_network/config/settings.py:48` | The validator submit interval (`weights_interval_seconds`) and the master epoch interval (`epoch_interval_seconds`) share the same default of 360 seconds. The supervisor's compute-only weights task also runs on the master's `epoch_interval_seconds` (`src/platform_network/supervisor/weights.py:89-101`). ## What happens each tick The validator fetches the master's vector from `GET /v1/weights/latest` (`validator/weights_client.py:18-30`). The runner rejects the payload if any check fails (`validator/normal_runner.py:93-107`): * the payload `netuid` does not match the configured netuid * the payload has expired (`expires_at` is in the past) * the payload is stale (older than `weights_freshness_seconds`) * the `uids` vector is empty * the `weights` vector is empty * `uids` and `weights` lengths differ If the payload passes, the validator calls `set_weights` with the hotkey wallet (`validator/normal_runner.py:84`, `bittensor/weight_setter.py:27-44`). The call uses `wait_for_inclusion=False` and `wait_for_finalization=False` (`bittensor/weight_setter.py:39-40`). A subtensor rejection is detected and logged, and the tick returns without raising (`validator/normal_runner.py:88-90`, `bittensor/weight_setter.py:7-18`). ## Freshness ceiling The freshness check compares `now - computed_at` against `weights_freshness_seconds`, which defaults to **720 seconds** (`validator/normal_runner.py:99`, `src/platform_network/config/settings.py:48`). A vector older than this ceiling is skipped, so the validator never submits a stale vector even if a fetch succeeds. ## If submission is not configured If the weights client, weight setter, or netuid is not configured, `submit_latest_weights` logs a warning and returns without submitting (`validator/normal_runner.py:64-71`). ## Next steps The submit-only on-chain process. Tune the cadence and freshness values.