Code-Verified Runtime Guide
Canonical runtime reference derived from executable code paths — service topology, API surface, security switches, and hardened production env profile.
This document reflects current behavior from executable code paths in:
minotaur_subnet/api/server.pyminotaur_subnet/api/routes/*minotaur_subnet/blockloop/loop.pyminotaur_subnet/validator/main.pyminotaur_subnet/miner/main.pyplatform/local_testnet/docker-compose.yml
It is intended to be the canonical runtime reference when older notes diverge.
Service Topology
Minotaur currently runs as a set of cooperating services:
- API server (FastAPI) at
:8080- Mounts
/v1routes for apps, orders, submissions, wallets, chains, and monitoring. - Can also run its own
OrderBook + BlockLoop + BenchmarkWorkerin-process.
- Mounts
- Validator service (aiohttp) at
:9100- Handles validator-specific endpoints (consensus, leader state, weights, legacy plan submission).
- Runs
BlockLooponly when leader (or whenFORCE_LEADER=1).
- Relayer service at
:8091(optional separate process)- Submits approved plans on-chain.
- Miner CLI
submit/statususe/v1/submissions*(git-based workflow). The inline source-submission endpoint (/v1/submissions/source) was removed in PR #599 — all submissions go through the git PR path.
Active Contract Path
The current live contract path is the AppIntentBase family of app contracts.
In local testnet and the default swap workflow, the canonical swap app is
DexAggregatorApp.
Notes:
DexAggregatorAppis the swap app seeded byplatform/local_testnet/seed.py.- The relayer submits
executeIntent(...)transactions toAppIntentBase-derived contracts.
Current Entrypoints
# API serverpython -m minotaur_subnet.api.server --port 8080
# Validator servicepython -m minotaur_subnet.validator.main --port 9100
# Miner agent loop (LLM strategy iteration)python -m minotaur_subnet.miner.main agent --validator-url http://localhost:8080
# Git-based solver submissionpython -m minotaur_subnet.miner.main submit --repo-url <url> --commit-hash <hash> --hotkey <wallet> --epoch <n> --validator-url http://localhost:8080Notes:
- The CLI subcommands are
agent,submit,status(norun, notest, nosubmit-legacy). - In local testnet,
/v1/submissions*are served by the API service on:8080. submitdiscovers the active round viaGET /v1/solver/roundand signs{repo_url}:{commit_hash}:{round_id}; the epoch is read from the round (--epochis an optional override). There is no/v1/statusepoch fallback.
API Server Surface (/v1)
Mounted routers in api/server.py:
apps,chains,wallets,monitoring,submissions,ordersintentsrouter exists in codebase but is not mounted in the current server bootstrap.GET /healthincludes sanitizedprovenance_policyandruntime_security_policyobjects (readiness booleans/counts only; no secrets).
Apps
Create / validate / deploy:
POST /v1/apps/(create;X-Admin-Keyor EIP-712owner_signature, rate-limited viaAPP_CREATE_RATE_PER_MIN)POST /v1/apps/validate(open preflight — no longer admin-gated; rate-limited viaAPP_VALIDATE_RATE_PER_MIN)POST /v1/apps/{app_id}/deploy(async by default;?wait=truefor the synchronous body. Wallet-signature or fee-payment authorized)GET /v1/apps/(per-chaindeploymentsmap + unifiedstatus,partialfor mixed states)GET /v1/apps/{app_id}/status
App-management, lifecycle & registry (wallet-signature auth — headers X-App-Auth-Signer/Signature/Nonce/Deadline; see the App-management API reference):
GET /v1/apps/{app_id}/admin-stateGET /v1/apps/{app_id}/auth-noncePUT /v1/apps/{app_id}/solidityPOST /v1/apps/{app_id}/deployments/{chain_id}/retirePOST /v1/apps/{app_id}/deployments/{chain_id}/float/depositPOST /v1/apps/{app_id}/deployments/{chain_id}/float/withdrawPATCH /v1/apps/{app_id}/deployments/{chain_id}/configPOST /v1/apps/{app_id}/deployments/{chain_id}/registry/allow-developerGET /v1/apps/{app_id}/deployments/{chain_id}/registry-calldataPOST /v1/apps/{app_id}/registration/request·.../approve(admin) ·.../reject(admin)
Other:
PUT /v1/apps/{app_id}/scoringGET /v1/apps/{app_id}/manifestGET /v1/apps/manifestsPOST /v1/apps/{app_id}/activatePOST /v1/apps/{app_id}/score
Orders
POST /v1/apps/{app_id}/ordersGET /v1/orders/{order_id}GET /v1/ordersDELETE /v1/orders/{order_id}POST /v1/orders/{order_id}/dry-runPOST /v1/apps/{app_id}/quoteGET /v1/orders/{order_id}/bridgeGET /v1/blockloop/statusPOST /v1/apps/{app_id}/prepare
Submissions
POST /v1/submissions(git-based; signature required)GET /v1/submissions/{submission_id}/statusGET /v1/submissions
GET /v1/submissions rows and the status payload now include outcome_code (machine-readable terminal-transition taxonomy — switch on this, not on the free-text reason), miner_uid (current-metagraph UID, null when unsynced/deregistered, PR #522), and a waitlist object when the status is waitlisted (PR #620). The waitlisted status is distinct from rejected (rotation-not-selected / benchmark-window-elapsed carry a next-round priority). The inline source endpoint was removed in PR #599.
Wallets / Chains / Monitoring
POST /v1/wallets/GET /v1/wallets/GET /v1/wallets/{address}/balancesGET /v1/wallets/{address}POST /v1/apps/{app_id}/fundPOST /v1/testnet/faucetPOST /v1/testnet/faucet_erc20GET /v1/chains(each chain now carriesapp_registry_address— the AppRegistry gate on that chain — alongsideregistry_address, PR #553)GET /v1/apps/{app_id}/monitor
Validator Service Surface (:9100)
The standalone validator currently exposes:
GET /healthGET /intents/availablePOST /intents/{app_id}/submitPOST /reloadGET /weightsGET /weights/historyGET /blockloop/statusPOST /orders/submitGET /ordersGET /intents/{app_id}/detailsGET /intents/{app_id}/scoresPOST /apps/{app_id}/quotePOST /consensus/proposalGET /consensus/infoGET /leader
Execution Flow (Order Lifecycle)
For orders submitted to /v1/apps/{app_id}/orders:
- Request normalization:
- Address parsing (plain
0x, CAIP-10, ERC-7930). - Token symbol/address resolution.
- Optional nonce auto-fill for managed wallets.
- Optional permit/approval helpers.
- Address parsing (plain
OrderBookreceives order (initial statusOPEN).BlockLoopclaims orders and executes:- plan generation (
SOLVED) - simulation (
SCORED) - JS threshold gate
- on-chain score gate
- consensus gate (
APPROVED) - relayer submission (
SUBMITTED)
- plan generation (
- Final outcomes:
- success:
FILLED - score/consensus/replay failures:
REJECTED - cross-chain source leg success:
BRIDGING - bridge failure:
BRIDGE_FAILED
- success:
Dual Scoring and Thresholds
Both checks are enforced in BlockLoop:
- JS score:
score(plan, state, context)againstapp.config.score_threshold(or global default). - On-chain score: simulation-derived
on_chain_scoreagainstapp.config.on_chain_threshold.
If either gate fails, order is rejected.
Solver Submission and Champion Adoption
Git-based (POST /v1/submissions)
Pipeline:
- Stage 1: static checks
- Stage 2: Docker build/import checks
- Stage 3: smoke tests
- Benchmarking
- Ranking + champion adoption
Adoption is relative and resolved by a fixed ladder (epoch/relative_scoring.py), high→low priority:
- Output (primary, always armed). Per-order
win/regression/matched(within ±0.1% /RELATIVE_TOL_BPS=10) /blind_spot_cover/dropped; adopt if net better on breadth:(wins + blind_spot_covers) − regressions ≥ DETHRONE_WIN_MARGIN (1). Regressions are tolerated within a 1% floor (FLOOR_BPS=100) and netted against wins — the older “any regression = reject / matching everywhere rejected” rule is gone. - Tie-breaks (fully-matched saturated tie only): gas (
GAS_MARGIN_BPS=200, pre-refund metered gas) → factorization (FACTOR_MARGIN=100,max_region_nodes) → deadwood (UNPRODUCTIVE_MARGIN=2000,unproductive_nodes). All armed ondevelop; each fires “by data” (inert until both champion and challenger carry the metric). The verdict dict carriesadopt_via(performance/gas/factorization/deadwood),factor_delta,deadwood_delta.
Hard vetoes (override every rung): n_catastrophic == 0 (no order cut > 1%) and n_dropped == 0. The blind-spot repeat bar is wired but disarmed (BLIND_SPOT_BAR_TTL_S = None).
Scoring definition — static quote. The benchmark no longer calls solver.quote() or runs a champion reference-quote pre-pass; it injects a static zero quote purely to satisfy the on-chain ABI, and scores on the raw per-order delivered output. The BENCHMARK_STATIC_QUOTE / BENCHMARK_REFQUOTE_CHECKPOINT flags and the legacy quote path were deleted (PRs #595/#600) — those host envs and /data/refquote_checkpoints.json are now inert.
Stage-1 screening floor (PR #585): new submissions are rejected too_entangled when their largest AST region exceeds MAX_REGION_NODES=4200, or dynamic_code on bare exec()/eval(). The metric is persisted even on reject so the miner sees the number. The standing champion is never re-screened.
Local Testnet Reality
From platform/local_testnet/docker-compose.yml:
- API is exposed on host
:8080 - Validator runs on internal network (
:9100), withFORCE_LEADER=1 - Relayer is exposed on
:8091 - There is no Docker miner service; miner agent is expected on host (
make miner-agent) - API is configured with
USE_EVM_RELAYER=1, and can run full order processing pipeline
Known Drift (Now Corrected Here)
miner.main runexamples are stale; current command isminer.main agent./v1/submissions*live on API server in local testnet.intentsAPI router is present in code but not mounted by default.
Submission Security Switches
Current policy controls in api/routes/submissions.py and worker/server wiring:
ENABLE_BENCHMARK_WORKER(defaultfalse) — explicitly enables background screening/benchmark worker.ENABLE_SOLVER_ROUND_COORDINATOR(defaulttruewhen the benchmark worker is enabled) — drives the closed-round solver lifecycle on the API server.SOLVER_ROUND_COORDINATOR_INTERVAL_SECONDS(default5) — how often the API server polls durable round state to resume explicitly closed rounds.SOLVER_ROUND_OPEN_SECONDS(default300) — how long the elected leader leaves a solver roundOPENbefore it auto-closes intake and freezes the replay cohort. Phase 1 of the two-phase round: a ~5-min OPEN window where submissions are collected and their images are built + distributed (benchmarking does not run yet — the round-anchored fork pin only seals onclose_epoch, sorun_oncedefers until close).SOLVER_ROUND_DECISION_EPOCHS(default20) — Phase 2 of the round: the post-close window (inEPOCH_SECONDSepochs) within which the leader must benchmark the champion + the round’s submissions and certify a finalist, i.e.decision_deadline_epoch = close_epoch + DECISION_EPOCHS. Must fit the post-close benchmark batch, which routinely runs well over 5 min with several submissions and/or a slow champion reference (each scenario re-quotes the champion); too small silently aborts contested rounds (certification_deadline_elapsed) the instant after the leader decides to adopt, instead of adopting. Leader-driven + broadcast (followers adopt the leader’sdecision_deadline_epoch), so keep it fleet-uniform across a rollout. With the default this gives a ~25-min two-phase round (5-min OPEN + up to ~20-min CLOSED).SOLVER_ROUND_ACTIVATION_DELAY_EPOCHS(default22) — when the certified champion activates:effective_epoch = close_epoch + ACTIVATION_DELAY_EPOCHS. Keep≥ SOLVER_ROUND_DECISION_EPOCHSso certification has fully landed before the swap takes effect.- Wall-clock epoch size is the fixed
EPOCH_SECONDSprotocol constant (60, inminotaur_subnet/epoch/clock.py) — used forclose_epoch/decision_deadline_epoch/effective_epochwhen native chain tempo and explicit block-based fallback are both unavailable. It is consensus-critical and not operator-configurable: the round-anchored fork pin anchors onanchor_epoch * EPOCH_SECONDS, so a divergent value causesPACK_HASH_MISMATCH. (Was theSOLVER_ROUND_EPOCH_SECONDSenv var; removed 2026-06-11.) SOLVER_ROUND_EPOCH_BLOCKS(unset by default) — optional block-based fallback epoch size; used only when native chain tempo is unavailable from metagraph/subtensor state.SUBMISSIONS_ACCEPTING(defaulttrue) — global kill switch for new submissions.SUBMISSIONS_API_KEY(unset by default) — if set, requires headerx-submission-api-keyon submission create endpoints.SOLVER_ROUND_INTERNAL_API_KEY(unset by default) — shared secret for validator-to-validator round control and champion proposal traffic via headerx-solver-round-internal-key.SUBMISSIONS_RATE_LIMIT_PER_MINUTE(default60) — per-route/per-principal create limit.ALLOW_SUBPROCESS_BENCHMARK(defaultfalse) — required forsolver_pathbenchmarking.SCREENING_BUILD_CONCURRENCY(default1) — bounds concurrent stage-2 Docker builds (PR #583; AST metrics also moved off the event loop).SUBMISSION_BENCHMARK_DETAILS_RETENTION(default300) — caps storedbenchmark_detailsto the N most-recent terminal submissions so the store can’t freeze the event loop (PR #569). In-flight submissions always keep details.SUBMISSIONS_MAX_ROUNDS_PER_FINGERPRINT(default0= OFF) — leader-gateway quota on distinct benched rounds per normalized code fingerprint, across all hotkeys (PR #594). Only benched statuses burn quota; a comment-only / nonce-only resubmit of an identical tree is rejected pre-build (fingerprint_repeat).SUBMISSION_INTAKE_ACK(default on;0disables) — posts a “Submission received” ACK on the miner’s PR to probePull requests: Writescope, so an under-scoped private-repo PAT is rejected at intake with a clear 400 rather than silently 403-ing days later (PR #526).- Removed:
ENABLE_SOURCE_SUBMISSIONSand the/v1/submissions/sourceendpoint (PR #599);BENCHMARK_STATIC_QUOTEandBENCHMARK_REFQUOTE_CHECKPOINT(PR #600). These envs are now inert. ALLOW_CHAMPION_HOT_SWAP(defaultfalse) — enables runtime champion swaps when explicitly set.CHAMPION_SWAP_TIMEOUT_SECONDS(default90) — timeout when starting champion Docker runtime.SUBMISSION_PROVENANCE_SIGNING_PRIVATE_KEY(unset by default) — if set, submission screening signs provenance with EIP-191 secp256k1.SUBMISSION_PROVENANCE_SIGNING_ADDRESS(optional) — expected signer address for the configured private key.SUBMISSION_PROVENANCE_ALLOWED_SIGNERS(unset by default) — comma-separated EVM addresses allowed to sign provenance.SUBMISSION_PROVENANCE_HMAC_KEY(unset by default) — legacy HMAC signer/verifier key (kept for compatibility).REQUIRE_SIGNED_PROVENANCE(defaultfalse) — when enabled, champion adoption/hot-swap requires valid provenance under configured verifier policy.REQUIRE_ASYMMETRIC_PROVENANCE(defaultfalse) — asymmetric-only policy: disallows HMAC provenance and requires allowed signer verification.VALIDATOR_HOTKEY_SS58(optional) — explicit hotkey override for solver-round metagraph leader election when the API service cannot load a local Bittensor wallet.- API startup now performs a provenance policy self-check and fails fast on inconsistent signer/verifier config.
ENFORCE_RUNTIME_SECURITY_PROFILE(defaultfalse) — strict production guardrail; startup fails if unsafe runtime flags are enabled (subprocess benchmarking, weak provenance verifier, missing API key/rate limit when accepting submissions).- Champion adoption policy only allows
GENESISor Docker-screened submissions with an immutable localimage_id(sha256:...). - Source (
solver_path) submissions can be benchmarked when enabled, but are never champion-eligible. - The benchmark worker performs replay scoring only; live champion activation is handled by the solver round coordinator via
EpochManager. - Round closure may still be forced manually via
POST /v1/solver/round/close, but when metagraph leader election is configured the elected leader now auto-closes rounds afterSOLVER_ROUND_OPEN_SECONDS, replay-evaluates them, auto-certifies finalists, and activates certified champions once the current solver-round epoch reacheseffective_epoch. - Champion certification collects real validator quorum through
POST /v1/solver/round/certify. The api service signs proposals whenVALIDATOR_PRIVATE_KEY+VALIDATOR_REGISTRY_<chain>+CHAMPION_REGISTRY_<chain>are configured; the validator set comes from on-chainValidatorRegistry.getValidators()and the quorum threshold fromChampionRegistry.quorumBps()(both refreshed every 60 s byProtocolConfig.refresh_loop). Followers answerPOST /v1/solver/round/consensus/proposal. Order consensus reads validator set + quorum from the same on-chainValidatorRegistryon the order-execution chain (Base in production). - Operators may abort a round explicitly via
POST /v1/solver/round/abortto retain the incumbent and reopen intake. - The leader and manual round-control endpoints now push explicit authenticated round state syncs (
/v1/solver/round/internal/close,/v1/solver/round/internal/certify,/v1/solver/round/internal/activate,/v1/solver/round/internal/abort) so followers persist closed/certified/activated/aborted state before failover. - The coordinator now enforces
decision_deadline_epoch: once the current solver-round epoch passes the deadline without a certificate, the round aborts and the incumbent stays active. - When metagraph sync can read native subnet tempo and
BlocksSinceLastStep, solver-round epochs now follow the real subnet epoch/tempo rather than a local approximation. /healthnow reportssolver_round_role, the currentsolver_round_epoch, the active epoch-clock mode/config, champion-consensus status, internal round-auth presence, and metagraph leader metadata when available.- When hot-swap is enabled, champion runtime loading uses isolated Docker sessions (
docker runharness protocol), not host-side Python imports.
Production Security Checklist
Recommended production posture:
- Disable subprocess benchmarking (
ALLOW_SUBPROCESS_BENCHMARK=0). - Require asymmetric signed provenance (no HMAC fallback).
- Require submission API key and non-zero rate limiting if submissions are enabled.
- Enable strict runtime profile validation so startup fails on insecure config.
- Verify
/healthreports bothprovenance_policy.valid=trueandruntime_security_policy.valid=true.
Example hardened env profile:
# Strict runtime guardrailsENFORCE_RUNTIME_SECURITY_PROFILE=1
# Submission intake controlsSUBMISSIONS_ACCEPTING=1SUBMISSIONS_API_KEY=__set_strong_shared_secret__SOLVER_ROUND_INTERNAL_API_KEY=__set_distinct_internal_shared_secret__SUBMISSIONS_RATE_LIMIT_PER_MINUTE=60ALLOW_SUBPROCESS_BENCHMARK=0
# Champion/runtime hardeningENABLE_SOLVER_ROUND_COORDINATOR=1SOLVER_ROUND_COORDINATOR_INTERVAL_SECONDS=5SOLVER_ROUND_OPEN_SECONDS=300# Wall-clock epoch width is a fixed protocol constant (60s) — not configurable.# Optional block-based epoch clock instead of wall-clock epochs:# SOLVER_ROUND_EPOCH_BLOCKS=360SUBTENSOR_URL=ws://127.0.0.1:9944NETUID=112WALLET_NAME=validatorHOTKEY_NAME=default# Optional if the API cannot read a local Bittensor wallet:# VALIDATOR_HOTKEY_SS58=5....VALIDATOR_PRIVATE_KEY=0x__this_validator_evm_key__# Peers come from on-chain ValidatorRegistry + metagraph axon discovery —# no VALIDATOR_PEERS env required (refactor landed PR #10 / 2026-05-25).VALIDATOR_REGISTRY_8453=0x__base_validator_registry__VALIDATOR_REGISTRY_964=0x__btevm_validator_registry__CHAMPION_REGISTRY_964=0x__btevm_champion_registry__ALLOW_CHAMPION_HOT_SWAP=1CHAMPION_SWAP_TIMEOUT_SECONDS=90
# Provenance policy (asymmetric-only)REQUIRE_SIGNED_PROVENANCE=1REQUIRE_ASYMMETRIC_PROVENANCE=1SUBMISSION_PROVENANCE_SIGNING_PRIVATE_KEY=__set_validator_signing_key__SUBMISSION_PROVENANCE_SIGNING_ADDRESS=__matching_signer_address__SUBMISSION_PROVENANCE_ALLOWED_SIGNERS=__comma_separated_allowed_addresses__SUBMISSION_PROVENANCE_HMAC_KEY=