# MINING — how to mine on the VidAIO subnet (SN85) How to run a miner against this stack, what you are scored on, and how to win honestly. The quickstart and the architecture overview are in the root [README.md](../README.md). > **Status.** The subnet is LIVE on Bittensor mainnet, **netuid 85** — register > and mine today. The production default is the real chain (`chain.mode: > bittensor`), constructing the `BittensorChainAdapter`. Before launch, real-GPU > miners earned on both inference tracks and participated in both earning > competition tracks across separately advertised hosts on live testnet. > `chain.mode: report` remains the default ONLY in test/dev/local overlays. --- ## What miners do Remote GPU backends may opt into bounded software recovery using `VIDAIO__MINER__REMOTE_GPU_ALLOW_CPU_FALLBACK=true` (default false). The worker must bind the response to the same input, track and variant and honestly report `cpu:ffmpeg-fallback` with GPU acceleration false. This does not relax deadlines, output caps, signed ingress or quality scoring. Software recovery uses the same CFR timestamp normalization as the scorer, not a frame-preserving re-timing of VFR input. It can produce different quality/size results; miners should test both tracks before enabling it. Upstream errors are logged with bounded, credential-redacted diagnostics. Miners transform video. The subnet (or the organic gateway, for paying customers) sends you a task; you return a processed file; the result is measured with real metrics (ffmpeg/libvmaf) and folded into your standing. **Scoring and weights are CENTRAL.** You are NOT scored separately by each validator any more. An owner-run **Scoring Authority** dispatches the challenges, runs the real measurement, folds the EWMA once (centrally), and each epoch publishes one immutable, on-chain-anchored **epoch log** of the authoritative scores. Validators then converge on that log (they submit the identical weight vector) AND independently AUDIT it. So your score is measured once, honestly, and is **independently recomputable and audited** — every scored item can be re-run over the real engine from the preserved audit files, and a substituted score or weight is provably caught (see the integrity invariant below). Nothing you do is judged by a private per-validator sampling any more. **Two pools, one pool per hotkey identity.** The subnet runs two tracks: - **compression** — re-encode the input smaller (byte ratio must shrink ≥ 1.25×) while keeping VMAF against the **served input** above the threshold. The quality term and the floor are measured against the exact file you received, so the VMAF you compute locally (libvmaf `vmaf_v0.6.1`, same canonicalization) is the VMAF you are scored on; the sealed-reference number is still published in every packet as `metrics.vmaf_pristine` for audit; - **upscaling** — upscale the degraded input (discrete factors 2× / 4×), scored on PieAPP quality + content length under per-factor file-size caps. **Inputs are per-challenge variants of the source.** Since `dag_version 8` the sealed reference itself is a seed-drawn transform of the original clip (horizontal flip or not, a small crop on every side, mild gamma/saturation/hue shifts and light seeded grain), and the input you receive derives from that reference. Your output must match the served input's geometry exactly (`target_width`/`target_height` in the task params); there is no public original that scores — a clip found online is the wrong geometry and colour, and the source-proximity check (below) still applies. A miner identity competes in exactly ONE pool, declared by the **TaskWarrant**: the validator probes `GET /warrant` and buckets every score for your hotkey there. To earn in both pools you run two identities (two hotkeys, two endpoints). A missing/garbage/timeout warrant answer means you are **skipped** for the round — the validator never defaults you into a track ([`vidaio/validator/README.md`](../vidaio/validator/README.md), the TaskWarrant fix). The reference miner declares its pool via `miner.warrant_track` (`vidaio/miner/config.py`). ### How you earn (plain-language tokenomics) Full engine: [`vidaio/tokenomics/README.md`](../vidaio/tokenomics/README.md); levers and their locked values in [`config/default.yaml`](../config/default.yaml). - Competition/crown is **live and earning**. Outside an active result window (**IDLE**), `80%` goes to inference and the fixed remaining `20%` goes to the canonical sink. A non-breakthrough result opens a seven-day **PODIUM** window (`60%` inference / `40%` competition); a breakthrough opens a seven-day **CROWN** window (`10%` inference / `90%` competition). The inference portion always keeps its internal **0.8 compression / 0.2 upscaling** split; neither track inherits the other's unused allocation. - Within a track, the **top 5** miners by accumulated score take a graded `5:4:3:2:1` rank curve; #1 earns five times #5, and rank 6+ takes nothing (`top_n_per_track: 5`). Scores below the absolute `minimum_payout_score: 0.05` earn zero even when fewer than five miners serve the track. - Your standing is an **EWMA** of round scores: `new = 0.75·old + 0.25·score` (`ewma_decay: 0.75`). One great round doesn't crown you; one bad round doesn't bury you — but zeros compound. - The **retention lever was removed for v1** (owner decision): there is no longer any multiplier for holding vs liquidating emitted alpha. The graded top-5 curve is based only on rank, with no retention reshaping. `burn_proportion` stays locked at 0. - Stake never increases the size of a reward: `alpha_stake_weigh_factor` is locked at 0. Schema v17 adds an optional inference eligibility floor, `tokenomics.payout_min_alpha_stake`, disabled by default (`0.0`). - Selection dedups by IP and coldkey (lowest uid wins) — running clones of one operation on one box or one coldkey does not multiply slots. - Any fixed track/podium/crown share with no eligible recipient is sent to the canonical sink/burn UID. It is not renormalized into another miner's pool. ### Distinct-content inference payouts (schema v17; ACTIVE on mainnet since epoch 24978, 2026-09-07 11:40 UTC) **Identical outputs share one scoring slot; differentiate your output to earn.** This also applies to honest miners running the same deterministic reference encoder on separate coldkeys and hosts. It is a content rule, not an accusation about ownership. The rule has been active on mainnet (netuid 85) since epoch 24978 (2026-09-07 11:40 UTC). It supersedes the older byte-exact-only dedup for economic purposes; the exact-byte path still exists as the first, trivial tier of the same check. For each ordinary challenge and track, the scorer hashes the entire canonical Y4M video it measured (the decoded, canonicalized stream, not the container bytes) and records 32 frame fingerprints: frames sampled at positions floor(i·(N−1)/31) for i = 0..31, each area-resized to 32×32 luma, transformed by an 8×8 DCT and reduced to a 64-bit perceptual hash (`content_fingerprint/1`). VMAF and compression rate play NO part in this comparison. Two eligible outputs match only when their canonical SHA-256 digests are identical under the same canonicalization plan (`canonical_content/3`, `exact_canonical_digest/1`). Fingerprints and encoded sizes are still recorded in the evidence but no longer create a match on their own. Rounds finalized under the earlier rules keep them and are verified under them: `canonical_content/1` also matched ≥30 of 32 fingerprints within 6 bits AND encoded sizes within 1% of the larger file; `canonical_content/2` used the same fingerprint test with a 0.2% size band. Connected matches form a group, including indirect matches through another output. The reference member is the minimum of the existing block-hash/hotkey ordering, using the authenticated challenge anchor fixed before dispatch. Its measured score is divided equally among all members of the group (`equal_share/1`): every member, the reference included, receives `winner_score / n` for that round as an archived `validator-content-duplicate/4` share packet; non-reference members carry the `DUPLICATE_CONTENT` reason with the share as their score. The group as a whole earns exactly one measurement, so copying an output is never better than producing a distinct one. The share enters the ordinary EWMA; it does not instantly erase an existing accumulator. Changing container tags or adding imperceptible noise is not a reliable way to obtain another slot. Materially different encodes can earn separately when they do not satisfy the matching rule. Only successfully measured, gate-passing outputs with a positive score and complete signed archive evidence are considered. A gate-passed zero (for example VMAF below the threshold) keeps its own zero and never claims a group's slot or suppresses positive outputs. If the authority cannot verify or archive a component's evidence before publication, it skips that component without a zero or a score fold. Auditors independently decode the archived outputs, reconstruct the complete declared scored roster and groups, and verify the salted reference member and every share against its archived measurement. The existing exact-byte duplicate path remains in force. The optional alpha floor uses stake at the epoch's exact close block, before inference IP/coldkey selection. Eligible miners retain the usual rank curve; a track with no eligible miners sends its allocation to the canonical sink. Competition podium awards are unchanged. The top-five curve, score floor, EWMA and pool percentages above remain unchanged. Audit disagreements remain report-only under the project design record; they do not automatically suppress authority weights. ### The competition track (the breakthrough crown) > **Live.** The shipping config enables competition emissions. Economic rank > comes only from the arithmetic mean of the exact committed score packets, with a stable > score/hotkey/uid tie-break. Stored human `final_rank`, manual disqualification, > eligibility, and review preferences do not affect payout. A CPU-only auditor opens the > corresponding audit bundles and independently rebuilds the result, crown, and weights. Besides always-on inference mining, there are compression, upscaling, object-removal and frame-interpolation **competitions**: sealed-sandbox code submissions evaluated against held-out content ([`vidaio/competition/README.md`](../vidaio/competition/README.md)). A contender submits **pinned code identity** — `repo_url + commit_sha + tree_sha` (`ContenderSpec`, `vidaio/competition/interfaces.py`) — that must build via its Dockerfile into an image honoring the run contract: ``` /bin/sh /app/run.sh ``` one output per input under the same digest-named filename, plain regular files only, exit 0. Evaluation runs in an isolated sandbox: no network, read-only root filesystem, no secrets, bounded output — your code sees inputs and writes outputs, nothing else. Builds do have network access; runs never do. **What is fixed before you enroll.** Every competition is described by a manifest whose digest is anchored on chain *before* enrollment opens, and the manifest is public. It carries: the four lifecycle times, the enrollment stake floor, the quality gate (`vmaf_threshold`), the compute envelope every contender gets (`sandbox_resources`: CPUs, memory, and `batch_timeout_seconds`, plus `allowed_gpus`; see "Time and disk limits" below), the result rules (`result_rules`, below), the archived baseline you are compared against (a competition may have none: then only its absolute score bars decide), and the ordered **commitments to the hidden clips** (`evaluation_item_commitments`). The clips themselves stay sealed until evaluation; the commitments let anyone prove afterwards exactly which clips were used and that none was swapped. **Score.** Each clip is scored with the same public compression formula as inference (`min(1, (0.7·(1 − rate) + 0.3·VMAF/100) / 1.12)`, zero below the VMAF gate or on a frame-count/geometry mismatch). Your competition score is the **plain mean of your per-clip scores**; ranking is by that mean with a stable hotkey/uid tie-break. Human review can flag a contender for investigation but never changes a payout. **Entering.** Enrollment is self-serve and signed: ``` python scripts/competition_enroll.py list --url https:// python scripts/competition_enroll.py enroll --url https:// \ --competition-id --repo-url https://github.com/you/solution.git \ --commit --tree \ --wallet-name --wallet-hotkey ``` The request body is only `{repo_url, commit_sha, tree_sha}` and is signed with your **hotkey** (your coldkey is never used). The validator verifies the signature, that the hotkey is registered on the subnet, and that its alpha stake — read from the chain, not from your request — clears the floor. One submission per hotkey per competition. The host and the schedule of each competition are announced in the VidAIO Discord (invite in the root [README](../README.md)); `list` shows every manifest, floor and allowed repository host. Git LFS and submodules are not supported. **One entry per operator.** A competition pays each operator once. Entries that share a coldkey, an advertised IP, a repository account (the `` in `https://github.com//`, whatever the repository), a source tree (the enrolled `tree_sha`, so forks of one solution) or a byte-identical output matrix count as one operator: only the lowest uid among them can hold a ranked, paid place, and the others are excluded from the payout. Extra hotkeys therefore never add places; enroll your best entry once. **Use a private repository.** Enrollment names a repository, a commit and a tree; it does not prove who wrote the code. Anything in a public repository can be copied, or even enrolled as-is by someone else, before the deadline. To let the evaluator read a private GitHub repository, add the machine user **`sn85competitionreader-collab`** under Settings → Collaborators. It is an ordinary account, not a GitHub App, and invitations are accepted automatically within a few minutes. GitHub only offers write access for collaborators on personal repositories; the reader never pushes, it fetches exactly the commit you enrolled (once for the sealed archive, once for the build). If you want strict read-only access, keep the repository in a free GitHub organization and add the reader as an outside collaborator with the Read role. Keep the reader invited until the builds are announced as done; an entry whose repository cannot be fetched cannot be evaluated. Evaluation is pinned to the enrolled commit and tree, so later pushes change nothing, and an enrollment cannot be edited: check the commit before you send it (`git rev-parse ^{tree}` prints the tree SHA). **Try it first.** `examples/competition_contenders/` ships two contender families you can copy: the GPU template, and `cpu_compression/` (x264, x265, VP9, SVT-AV1 fixed and a quality-searching SVT-AV1) with `materialize_cpu.py`. The header of `cpu_compression/run.sh` states the full run contract; you can run it locally against any folder of sha256-named clips before you enroll. **Practice clips and past data.** A public practice pack (eight lossless clips: AI-generated, animation, screen recording, archive film and space footage, with odd frame sizes and frame rates) is attached to the [`practice-pack-001` release](https://github.com/vidAio-subnet/miner-vidaio/releases/tag/practice-pack-001) of the public miner repository. It is similar in kind to competition content but shares no clip with any hidden set. Every inference clip the validator has scored is public as well, with each miner output, the released pristine source and the full score packet, in the evidence bucket `https://vidaio-sn85-evidence.s3.eu-central-1.amazonaws.com` (anonymous read, no listing). Start from `finalized/epoch=/log.json` (first epoch 24920; epoch `N` closes at block `8994622 + 360 * (N - 24920)`), follow `audit_manifest.per_uid` to each `audit_bundle///`, and the bundle names the `challenge_input`, `miner_output`, `released/reference_original` and `score_packet` objects under the same `///` layout. **Checklist: what loses you a clip, a batch or the whole entry.** Every limit below is enforced by code, the same for everyone, and an enrollment cannot be edited afterwards. *The whole entry is lost when:* - the repository cannot be fetched after the deadline (reader not invited, repository deleted or renamed, commit not pushed), or the fetched commit/tree does not match what you enrolled; - the tree contains `.gitmodules`, symlinks or special files, or is larger than 512 MiB. Keep large weights out of Git: download them in the Dockerfile (builds have network) and verify a checksum; - the image does not build. `Dockerfile` must be at the repository root and build for linux/amd64 within 30 minutes; the image may be at most the manifest's `container_size_limit_gb` (measured after the build: the apparent size of every file in the image, 1 GB = 10^9 bytes). Prefer prebuilt encoder binaries to compiling from source; - the Dockerfile uses something the sandbox builder does not support. The builder is not Docker/BuildKit: `ADD` is rejected (use `COPY`, or download inside a `RUN`), `USER` is ignored, and an image reference in `FROM` or `COPY --from` must carry a tag **or** a digest, never both (`name:tag@sha256:...` fails with "Docker references with both a tag and digest are currently not supported"; `name@sha256:...` works). A build that passes locally can still fail here, so keep the Dockerfile to plain `FROM`/`COPY`/`RUN`/`ENV`/ `WORKDIR` and test it against these rules before pinning your commit (`python scripts/competition_precheck.py --commit ` checks the exact commit you are about to enroll against all the entry-losing rules in this list); - the image has no `/bin/sh` or no `/app/run.sh`. Your `ENTRYPOINT`/`CMD` are ignored, no environment variable is injected, there is no network at run time, and every call runs in a fresh sandbox: nothing you write survives from one batch to the next. *A whole batch scores zero when:* - `run.sh` runs longer than `batch_timeout_seconds`. That budget bounds ONE whole `run.sh` call, and one call receives `evaluation_batch_size` clips (five clips per call when the manifest says 5). It is wall-clock time and includes any quality measurement you run yourself, so keep a margin and track elapsed time inside the script; - `run.sh` exits non-zero. Do not let one failing clip abort the script (`set -e` does exactly that): catch the failure and fall back to a safe encode for that clip; - the output directory exceeds 512 MiB for one file or 2 GiB in total **at any moment of the run**, temporary files included, or holds more than 4096 entries. Work under `/tmp` and delete temporary files. The local Docker runner used for self-testing caps `/tmp` at 256 MB, so prefer pipes to large intermediate files; - stdout plus stderr exceed 8 MiB. Run encoders with `-loglevel error` or redirect their logs; - an output is a symlink or anything other than a plain regular file. *If the platform stops your sandbox* (`run.sh` is killed by a signal and the sandbox itself is gone, for example a preemption), the batch is rerun on a fresh sandbox, at most twice; it scores zero only if that keeps happening. A `run.sh` killed while its sandbox keeps running (for example out of memory) is scored as a non-zero exit. Do not trap SIGTERM to exit 0: a batch that exits 0 is scored on the outputs it wrote, and missing clips score zero. *One clip scores zero when:* - its output is missing, empty, or not named exactly like the input. Inputs are named by their sha256 and have **no file extension**, so tell your tools the container explicitly (`-f mp4`) and let them probe the input format; - the codec is not H.264, HEVC, VP9 or AV1 (nothing else is accepted, whatever the decoder could read); - width, height or the number of frames differ from the input, the duration differs by more than 5 %, or the timestamps are inconsistent. Do not resample the frame rate: clips come at 24, 29.97, 30 fps and others, with frame sizes that are not always a multiple of 8 or 16. Pad internally if your encoder needs it, but the decoded output must have the input's size; - mean VMAF (`vmaf_v0.6.1`, whole clip, against the input you received) is below the manifest's `vmaf_threshold`. A second VMAF model and chroma/tone checks run as well: sharpening, contrast or colour tricks that inflate VMAF zero the clip. Outputs are compared after conversion to 8-bit 4:2:0, so 10-bit encodes are fine. **What your code may do.** Anything that fits the contract and the compute envelope. Analysing each input and choosing the encoder, preset, filters or GPU path per clip, encoding several candidates and keeping the best, and measuring VMAF yourself to search for the smallest file that clears the gate are all allowed and expected; clips differ a lot (live action, animation, AI-generated footage, film grain, screen recordings), so a single fixed setting will do badly somewhere. Everything you need must be inside the image: a run has no network. **Two things that cost real entries a clip.** (1) Check your encoder on odd frame sizes. Distribution packages can be old: the SVT-AV1 1.7 shipped with Ubuntu 24.04 crashes on frame widths that are not a multiple of 8, and that ffmpeg has no `libvmaf` filter, so a quality search silently degrades to a fixed setting. The example image installs a current static ffmpeg build instead and fails the *build* when a needed encoder or filter is missing. (2) Leave a margin above the VMAF gate. The scorer measures the full clip with its own pinned libvmaf build and model; your measurement can differ by a few tenths, and a clip below the gate scores zero. **Object-removal competitions (track `removal`).** The contract above is unchanged (same `run.sh`, same limits); only the items and the score differ. - *Input:* each item is ONE Matroska file (named by its sha256, no extension) with two video streams. Stream 0 is the clip with an object in it (FFV1, 4:2:0, at most 1280×720, constant frame rate). Stream 1 is the per-frame mask (FFV1 gray, same size and frame count; luma above 127 marks the pixels to reconstruct). Read them with `-map 0:v:0` and `-map 0:v:1`. Masks are object-shaped and change from frame to frame; a frame whose mask is empty (the object is out of view) must come back unchanged. - *Output:* one MP4 per item with a single H.264, HEVC, VP9 or AV1 stream of the same width, height, frame count and frame rate. Pixels outside the mask must stay what you received: the largest per-frame mean absolute RGB change outside the mask must not exceed 3.0, so encode losslessly (`libx264 -qp 0`) or near-losslessly (all-intra `-crf 8`), and paste your model's result back inside the mask only. Output size is not scored, but the 512 MiB / 2 GiB caps still apply. - *Score:* only the masked region is measured, against the clean original (sealed during the competition, published afterwards). The validator first measures two free answers: its own fill (per-pixel temporal median of the unmasked frames, Telea inpainting where the background is never visible) and the clip returned unchanged. The floor is the better of the two on each metric (higher PSNR, lower LPIPS), so doing nothing never scores. An item scores zero when your region PSNR is not at least 1.5 dB above that floor, when your region flickers (temporal warp error above 5× the original's), when pixels outside the mask changed, or on a size/frame-count mismatch. Otherwise it scores `0.6 · min(1, (PSNR − floor PSNR) / 8 dB) + 0.4 · (floor LPIPS − LPIPS) / floor LPIPS` (clamped to [0, 1]). The code is `vidaio/scoring/removal.py` and `vidaio/scoring_worker/removal_backend.py`. - *Content:* the hidden items mix difficulty levels: static objects, moving objects the mask follows, objects that leave the frame (or vanish) and come back, and hard cases (large, fast, zooming or several objects). The clean background is often visible in other frames, so methods that propagate real pixels across time (flow-guided video inpainting) do far better than inpainting each frame on its own; check the licence of any model you ship. - *Start from* `examples/competition_contenders/removal_example/`: a complete CPU entry that implements the contract (it is the free fill, so it scores about zero) with a local self-test. **Frame-interpolation competitions (track `interpolation`).** The contract above is unchanged (same `run.sh`, same limits); only the items and the score differ. - *Input:* each item is ONE Matroska file (named by its sha256, no extension) with one video stream (FFV1, 4:2:0, at most 1920×1080, constant frame rate): a genuine high-frame-rate clip with every other frame removed, so its rate is half the original (for example 30 fps made from 60 fps). N input frames. - *Output:* one MP4 per item with a single H.264, HEVC, VP9 or AV1 stream of the same width and height, **twice the input frame rate** and **exactly 2N − 1 frames**: frame 2k is input frame k, frame 2k + 1 is your new frame between input frames k and k + 1. The input frames must come back unchanged: the largest per-frame mean absolute RGB change between output frame 2k and input frame k must not exceed 3.0 (this also catches an output shifted off the grid), so encode near-losslessly (`libx264 -crf 8`). Drive your model on the exact grid: wrappers that skip near-duplicate frames, repeat frames across cuts or round the frame rate change the frame count, and the item scores zero. - *Score:* only the inserted frames are measured, against the real frames that were removed (the full-frame-rate original is sealed during the competition and published afterwards): PSNR over all their pixels, SSIM on luma, LPIPS-VGG on every 4th inserted frame. The validator measures two free answers the same way, repeating the previous frame (hold) and averaging the two neighbours (blend), and the floor is the better of the two on each metric. Each term credits only the gain over that floor, as a share of what is left to gain: `s_psnr = min(1, max(0, (PSNR − floor PSNR) / 8 dB))`, `s_ssim = clamp((SSIM − floor SSIM) / (1 − floor SSIM))`, `s_lpips = clamp((floor LPIPS − LPIPS) / floor LPIPS)`, and the item scores `0.3 · s_psnr + 0.3 · s_ssim + 0.4 · s_lpips`. Returning the hold or the blend scores zero; an item also scores zero on a frame-count, size or kept-frame violation. The code is `vidaio/scoring/interpolation.py`, `vidaio/scoring/interpolation_formula.py` and `vidaio/scoring_worker/interpolation_backend.py`. - *Speed counts* when the manifest's `result_rules` carry a speed term (all four `speed_*` fields; frame interpolation only). Every `run.sh` call is timed by the validator's runner, from start to exit, on the single pinned GPU type and the anchored CPU/RAM envelope (`allowed_gpus` has one entry), and the times are added over all your batches. A batch your solution fails (non-zero exit, timeout, unsafe output) counts its full `batch_timeout_seconds` (as raised by any execution amendment), so failing fast never buys speed. When a batch's Sandbox is stopped and the batch is rerun, the stopped attempt counts too, up to the time of the run that completed, so bringing your own Sandbox down never buys a faster run. Model loading inside `run.sh` counts too. With `spf` = total seconds / `speed_inserted_frames`: `speed_score = clamp(log(speed_slow_spf / spf) / log(speed_slow_spf / speed_fast_spf), 0, 1)` is 1 at or under `speed_fast_spf` (for example 1/30 s per inserted frame: real time for 30 to 60 fps) and 0 at or over `speed_slow_spf`, and every 2x speed-up is worth the same. Your competition score is `mean item score x (1 - speed_weight + speed_weight x speed_score)`; the crown and podium bars apply to that number. The per-batch times are committed with the result (attested by the validator that ran the sandboxes) and the auditor re-derives the score from them. - *Content:* the hidden clips are genuine high-frame-rate footage with real motion (no duplicated or static frames), with easy and hard cases mixed: camera pans, fast subjects, occlusions, thin structures and fine texture. Methods that estimate motion between the two neighbours do far better than blending. An entry must compute its new frames from the input it is given: shipping video, frames or any other copy of existing footage in the image to look the answer up is not a solution, and an entry found doing it is rejected and its operator barred from later competitions. - *Licences: commercial use only.* The winning solution runs on the VidAIO website, so everything an entry ships (code, libraries, model weights) must be under a licence that allows commercial use (Apache-2.0, MIT, BSD, ISC, MPL-2.0, CC0/Unlicense, CC-BY-4.0, or GPL/LGPL such as ffmpeg), judged on what is shipped, not on training data. Non-commercial, research-only, no-derivatives, AGPL/SSPL or unlicensed components are refused, including fine-tuned, merged, adapted or distilled versions of such models. Your own code must be released under Apache-2.0 or MIT (a `LICENSE` file at the repository root at the enrolled commit), and a `LICENSES.md` at the root must list every third-party component (name, source URL, licence, location in the repository). What is reviewed must be what is built, so every file your Dockerfile downloads must be pinned by content: check its sha256 in the same `RUN` step (`echo " model.pth" | sha256sum -c -`), or use a Hugging Face commit revision, a git commit, or `pip install` with `==` versions or `--require-hashes`. An entry with an unpinned download is rejected. `python scripts/competition_precheck.py --commit --commercial` flags the common cases. Entries are checked in the 24 h between the enrollment deadline and the start of evaluation; an entry that breaks these rules is rejected then, publicly with the reason, and never scored. A violation found later, until the competition completes, takes the entry out the same way: it leaves the scoring and the result. - *Start from* `examples/competition_contenders/interpolation_example/`: a complete CPU entry that implements the contract (it is the blend, so it scores about zero) with a local self-test. **Result rules.** A competition may publish its own `result_rules` in the anchored manifest: `crown_margin` (the relative win over the rerun baseline that makes the result a CROWN), `crown_min_score` (an absolute score the winner must also reach to crown), and `podium_min_margin` / `podium_min_score` (what a contender must reach to hold a paid rank at all). They are identical for every contender, fixed before enrollment, copied into the epoch evidence and re-checked by auditors against the anchored manifest. When the block is absent the protocol default applies (crown at an inclusive 5% margin, every ranked contender eligible for the podium). Margins are measured against the rerun baseline only, never against the runner-up, so entries never count against each other. If the rerun baseline scores zero on the hidden clips, the relative margins cannot discriminate and the absolute bars decide alone: `crown_min_score` for a CROWN and `podium_min_score` for a paid rank. A competition may also be announced **without a baseline** (its manifest has no `baseline` block): it must then anchor both `crown_min_score` and `podium_min_score`, and those two numbers decide on their own — reach `podium_min_score` to hold a paid place, and the first place crowns when it also reaches `crown_min_score`. Competition payouts use the latest globally applied result for seven days (a newer result replaces it). Since tokenomics v3 (2026-09-27) inference earns nothing and a result pays **100 % of miner emissions** by default: a result that does not meet the crown rule opens a **PODIUM** window whose pot is split 50/24/13/8/5 over the top five qualifying places; a result that meets it opens a **CROWN** window split 90/4/3/2/1. Unfilled places go to the filled ones in proportion; a result with no qualifying contender closes the window and the subnet burns until the next result; a deregistered paid hotkey sends its share to the sink, and an entry whose hotkey is not registered when the result is applied is left out of the ranking. **Tokenomics v4 (from the frame-interpolation competition on).** Competitions run every two weeks and a result pays for **two weeks** (336 hours), so each result covers the time until the next one. A gap with no active result no longer burns everything: **50 % burns and 50 % goes to the licensing treasury**, a registered hotkey that pays licensing fees to winners whose solutions run on the VidAIO website. While a window is active the treasury keeps **0.5 %** so it stays registered, and the window's pot and split apply to the other 99.5 % (a 90/4/3/2/1 crown pays 89.55/3.98/2.985/1.99/0.995 % of emissions). Every epoch log names the treasury hotkey it pays (`treasury_hotkey`), and auditors check it against their own policy; an unregistered treasury's share burns. Every competition anchors these values in its manifest (`result_rules`), so check the manifest of the competition you enter: the pot and the split may differ from the defaults. When a competition has a baseline, the executable comparison floor is that archived baseline rerun on the same hidden matrix. Every contender uses the same enrollment, scoring, audit, and payout path. `tokenomics.competition_emissions_enabled` is retained only as an emergency off switch; emissions-on is the normal state. --- ## Requirements - **Hardware**: the reference backends are plain ffmpeg/x264 — any ffmpeg-capable CPU box works. A GPU is required only if YOUR approach needs one (learned restoration, super-resolution, etc.); nothing in the protocol assumes it. `ffmpeg` on `PATH` (the reference miner shells out to it; `miner.ffmpeg_path`). - **Network**: your task endpoint must be reachable by validators (and the gateway, if you serve organic traffic). Production validators accept globally routable literal axon IPs; real `axon.port` is preferred over the configured fallback, and IPv6 literals are bracketed correctly. - **HTTPS on the advertised port, with a publicly trusted certificate.** Mainnet validators dial every miner as `https://:` with normal certificate verification (`validator.miner_url_scheme=https` fleet-wide). A port that answers plain HTTP, or presents a self-signed certificate, fails the `GET /warrant` probe every round — the authority logs `warrant probe failed; track stays unknown` and the miner is **skipped, never dispatched to, never scored**. The certificate must be valid for the literal IP you advertise (Let's Encrypt issues short-lived IP certificates; Caddy or certbot can automate renewal — make the renewal hook restart the TLS edge). Verify from outside the box with `curl https://:/warrant` **without** `-k`; the reply must be `{"track": "compression"}` or `{"track": "upscaling"}`. - **Serving hotkey wallet**: artifact protocol v2 signs every response. The miner role loads its own hotkey-only wallet through `chain.*`; `chain.validator_hotkey` and `miner.artifact_hotkey` must both equal the registered hotkey advertised for this endpoint. Never put a validator or coldkey wallet in the miner service. Create a FRESH hotkey per mining identity (`btcli wallet new_hotkey`), register it on the subnet (`btcli subnet register --netuid 85`), and advertise your exact public endpoint (`scripts/advertise_miner.py` — the metagraph must read back the same literal IP and port you serve on). The coldkey stays offline; only the hotkey-half lives on the mining box. - **Registered-hotkey auth (P2)**: competition enrollment is SELF-SIGNED — the enrolling hotkey signs its own enrollment request (Scheme A headers, see `vidaio.services.hotkey_auth.sign_request_headers`), must be registered on the subnet, and must clear the configured minimum alpha stake. An operator can no longer enroll a hotkey on its behalf; a signer may only enroll itself. - **Disk**: task work dirs under `miner.work_dir`, swept after `task_dir_ttl_seconds` (900 s default). Inputs up to 2 GiB (`miner.max_input_bytes`). - **Ingress clock**: the server allows 60 seconds by default (configurable only within `(0, 300]`) from admission through complete upload staging/fsync. Slow uploads receive HTTP 408 and their partial task directory/capacity slot are released. ### The wire contract you must serve Authoritative source: [`vidaio/services/protocol.py`](../vidaio/services/protocol.py). Two routes, on your API port (default **8300**): **`POST /v1/task/artifact`** — bounded path-free byte exchange: ```jsonc // canonical JSON before base64url encoding into X-Vidaio-Task-Metadata { "task_id": "", // echo it back EXACTLY "track": "compression", // or "upscaling" "input_digest": "", "params": {"...": "track params, e.g. upscale factor / bitrate cap"}, "deadline_seconds": 300.0 // seconds before you are scored absent } ``` The request body is the raw input stream (`application/octet-stream`, with an exact `Content-Length` and a running byte counter). Metadata is capped at 4 KiB before encoding so it fits normal proxy header limits. Production uses artifact version `2`: alongside the metadata header it carries `X-Vidaio-Validator-Hotkey`, `X-Vidaio-Miner-Hotkey`, request timestamp, 128-bit nonce, input size, and request signature. The canonical signature binds all task metadata, input digest/size, both identities, timestamp, and nonce. Before reading the body, the miner checks that the request names itself, verifies the signature, refreshes chain state, and requires exactly one current neuron with validator permit for the signer. A stale/duplicate nonce is rejected. Both the global live replay cache and a smaller per-validator quota are hard bounds; exhausting either returns 503 `artifact_auth_capacity` instead of evicting an unexpired entry or allowing one validator to consume all capacity. Every miner start also fences timestamps at `start_time + artifact_request_future_skew_seconds`. A request at or below the fence is rejected before body ingress with 425 `artifact_auth_starting`; retry after the bounded roughly `future_skew + 1 second` startup blackout using a fresh timestamp and nonce. That prevents replay of a still-fresh request captured before an in-memory cache restart. The cache is process-local, so one miner hotkey must terminate at one ingress process. Multiple replicas sharing a hotkey behind a load balancer are unsafe until a shared replay store exists: the same captured request could land once on each replica. The response body is the raw output stream. Headers carry version 2, exact task id, output sha256, output size, processing seconds, miner hotkey, and response signature. The signature binds the complete signed request plus output digest/size/processing time; the validator verifies it against the chain-attributed miner hotkey before publishing the download. Unsigned artifact v1 exists only behind `miner.allow_unsigned_artifact_v1: true` in isolated report/tests and production forbids that switch. **`GET /warrant`** — `-> {"track": "compression" | "upscaling"}`, your pool declaration. JSON absolute-path `POST /v1/task` and its pre-versioning `/task` alias are DEPRECATED local-test compatibility routes. They are absent by default, require `miner.enable_legacy_path_routes: true` in a test fixture, and that opt-in is rejected in production. Obligations that get you zeroed or skipped if violated: - **Echo `task_id` exactly.** The validator authors it (`"{challenge_id}:{uid}"`); any other id in your response is zeroed (`task_id_mismatch`). - **Digest discipline.** Verify `input_digest` over the bytes received. Return the real output sha256 header; the validator recomputes it while streaming, also verifies task id/size/version, and atomically publishes only a fully bound response. - **Deadlines.** `deadline_seconds` (default 300 s) is your budget; the validator's own request timeout (`miner_request_timeout_seconds`, 300 s default) hard-bounds it. A timeout is a zero-scored empty response. It cannot enlarge the miner's independent `artifact_ingress_timeout_seconds` upload cap. - **Capacity honesty.** Over capacity, answer `429 busy` (`miner.max_concurrent_tasks`) — the round scores you absent, which beats queueing unbounded ffmpeg jobs and timing out on everything. - **Auth and public-edge protection.** `miner.api_token` / `X-Miner-Token` is for a controlled caller fleet. Do not share one secret subnet-wide with permissionless validators or miners. A permissionless miner leaves that optional extra bearer unset; protocol v2 authenticates current validators and miner responses by hotkey. Keep the endpoint behind a hardened reverse proxy/edge enforcing request/connection-rate, timeout, and network-abuse limits. Set the fleet-wide `validator.miner_url_scheme=https` only when every advertised literal IP presents a valid public certificate; otherwise the public hop is explicitly HTTP (artifact-v2 still authenticates both sides). The miner's own signature/replay/body/concurrency caps remain mandatory. - **Cleanup.** The canonical miner deletes its task directory after the output stream; TTL is crash recovery. The validator deletes its private download after scoring and archival on every success/failure/cancellation path. --- ## Running the reference miner The public build runs the miner role against the real chain: ```bash python scripts/service_entrypoint.py reference-miner ``` Registration, endpoint advertisement, and hardening are covered in the [Mainnet](#mainnet) section below. > **Chainless simulation.** The development tree carries a one-command local > stack (a chain simulator plus the full scoring/audit topology) used to > exercise the whole loop offline — challenge production → your encode → three > libvmaf runs → central EWMA fold → epoch-log finalize+anchor → validator > converge → auditor recompute. That tooling is a development tool and is not > included in this repository; the public entrypoint refuses simulator modes > and runs the miner only against the real chain. Miner config keys (section `miner`, schema `vidaio/miner/config.py`, env override `VIDAIO__MINER__`): | Key | Default | Meaning | |---|---|---| | `http_port` / `metrics_port` | 8300 / 9106 | API and health/metrics ports | | `warrant_track` | `compression` | THE pool this identity competes in | | `compress_crf` / `compress_preset` | 28 / `medium` | x264 levers, compression track | | `upscale_crf` / `upscale_preset` | 16 / `medium` | x264 levers, upscaling track | | `max_concurrent_tasks` | 2 | Past it: 429 `busy` | | `max_input_bytes` | 2 GiB | Bigger remote artifact bodies refused (413) before work; deprecated local path uses 422 | | `max_output_bytes` | 4 GiB | Output rejected before response streaming if it crosses the auditor-compatible bound | | `artifact_ingress_timeout_seconds` | 60 | Server-owned admission→receive/staging/fsync cap; constrained to `(0,300]` | | `artifact_hotkey` | `""` | Serving miner ss58; production requires it equal the loaded `chain.validator_hotkey` wallet | | `allow_unsigned_artifact_v1` | `false` | Explicit report/test compatibility only; production forbids unsigned v1 | | `artifact_request_max_age_seconds` / `artifact_request_future_skew_seconds` | 120 / 5 | Signed-request freshness window and tolerated positive clock skew | | `artifact_replay_cache_entries` | 10000 | Bounded live `(validator hotkey, nonce)` replay entries; full cache fails closed | | `artifact_replay_cache_entries_per_validator` | 256 | Per-validator live nonce quota; must be no larger than the global cache | | `artifact_validator_snapshot_max_age_seconds` | 300 | Oldest metagraph snapshot accepted for current-validator authorization | | `task_dir_ttl_seconds` | 900 | Output grace window before the reaper | | `api_token` | `null` | Optional controlled-fleet `X-Miner-Token`; do not distribute one shared secret to permissionless subnet participants | ### How scoring works against you — what gets you zeroed **Submit a real, self-contained video file.** The scorer opens submissions only as Matroska/WebM, MP4/MOV-family or IVF, over the local-file protocol only. Anything else — playlists and manifests (ffconcat, HLS, DASH), NUT, AVI, MPEG-TS, image sequences, files that reference other files — fails to open and the item scores zero. Scoring engine: [`vidaio/scoring/README.md`](../vidaio/scoring/README.md). Validity gates run FIRST and are absolute — any failure forces score 0 with a machine-readable reason code recorded in the audit packet: - **The rate gate** (compression): byte ratio ≥ `0.80` → `COMPRESSION_RATE_TOO_HIGH`. You must shrink by at least 1.25×. - **The VMAF floor/threshold** (compression): VMAF against the PRISTINE reference below `threshold − 5` → `VMAF_BELOW_FLOOR`; below the threshold → `VMAF_BELOW_THRESHOLD`. The production default threshold is 90. Note it is measured against the pristine original, not the degraded input you received — a miner that only re-encodes and never restores will honestly zero on heavily degraded draws. - **The model-delta gate**: primary VMAF vs the NEG ("no enhancement gain") model delta > 3.0 → `VMAF_MODEL_DELTA_EXCEEDED`. Both anti-gaming model runs use the **miner input** as their reference, so the gate measures what the miner added rather than transformations already present in its challenge payload. The 3.0 threshold is a calibrated constant, measured against the real degradation space. - **Dedup (two tiers)**: (1) byte-identical outputs across miners (exact verified SHA-256 digest) → `REPLAY_DUPLICATE`; (2) same-content outputs under `canonical_content/3` — identical canonical-stream digest under the same canonicalization plan (the earlier `canonical_content/1` and `/2` rules also matched ≥30 of 32 sampled frame fingerprints within 6 bits AND encoded sizes within 1% or 0.2% of the larger file, and rounds finalized under them are verified under them) → `DUPLICATE_CONTENT` (see "Distinct-content inference payouts" above). In the exact-byte tier the single paid winner of a duplicate group is the minimum of the finalized challenge-anchor hash salted with each miner hotkey (`anchor_hash_hotkey/1`); in the same-content tier that minimum is the reference member whose measured score is split equally among all members (`equal_share/1`). Arrival timing, UID and the validator cannot choose the reference; shares and zeros are minted only with the signed receipts, outputs and the content witness available to auditors. - **Source proximity** (compression, decided per round, not per item): every measured packet publishes `vmaf_residual` = VMAF(output, pristine) − VMAF(output, input) and `chroma_residual` = PSNR_UV(output, pristine) − PSNR_UV(output, input) (mean per-frame U/V-plane PSNR on the canonical streams). An output whose residuals BOTH sit above the round medians by the fixed margins (+0.25 VMAF and +0.10 dB chroma) while its absolute `vmaf_residual` is positive is closer to the sealed pristine reference than to the input it was served — the signature of an encode made from the pristine — and is zeroed with `SOURCE_PROXIMITY`. The decision needs at least three measured outputs in the round, is relative to the round (clip effects shift everyone), and is minted only with a canonical roster witness (`validator-source-proximity/1`) that auditors re-derive from the archived original packets and the released media. Encoding the served input — however aggressively — keeps both residuals at or below the population. - **Non-finite / missing metrics**: fail closed — `METRIC_MISSING` / `METRIC_NON_FINITE`, never a silent pass. - **Stream validity**: frame count, duration, dimensions, PTS consistency (`FRAME_COUNT_MISMATCH`, `STREAM_*` codes); upscaling adds per-factor file-size caps (`FILE_SIZE_CAP_EXCEEDED`) and `UNSUPPORTED_SCALE_FACTOR`. - **Perceptual-manipulation gates** (tone/grayscale/chroma: `TONE_MANIPULATION`, `COLOR_GRAYSCALE`, `CHROMA_UV_MANIPULATION`): deterministic CPU/OpenCV checks that are required in production and local release testing. - Miner-attributable timeout, transport/429, protocol, task-id, output-digest and receipt failures fold an explicit zero only when the authority persists a canonical observation binding its signed artifact-v2 request, finalized anchor, target hotkey/endpoint and deadline. This stops selective non-response from freezing EWMA. The negative network fact remains a validator observation (not a Byzantine proof), but it is immutable and publicly disputable. A miner's authenticated restart fence that consumes the whole signed request deadline is miner-attributable and also folds zero. Validator-side faults—scoring worker, audit store, chain/challenge service, or local input—remain excused. The upscaling track uses PIQ PieAPP. The release/auditor path runs it on CPU with digest-pinned, preloaded weights. Miners are free to run whatever GPU stack their approach needs; the launch scorer and every auditor remain CPU-only. CUDA scorer mode is development-only until a fresh Modal scorer proves exact CPU packet parity and the production guard is changed under a versioned release. ### Reading your scores Your weight share IS your standing after EWMA + rank curve — on mainnet, read it straight from the metagraph (`btcli`, or any metagraph explorer). Per-round detail (gate failures with reason codes, VMAF/rate numbers) is in the validators' structured JSON logs and in the archived ItemScore packets, which any third party can open and recompute from the audit store ([`vidaio/audit/README.md`](../vidaio/audit/README.md)). --- ## How to beat the reference miner honestly The reference miner is a floor, not a bar: it re-encodes (CRF, in whichever of h264/hevc/av1/vp9 the task asks for — x264 when it asks for nothing) and does not restore. Measured on the dev content, its binding constraint is VMAF against the pristine reference — it wins only the draws whose degradation a re-encode can survive. Beat it by: - **Better encoding**: smarter codec/preset/CRF choices per content, two-pass or content-adaptive rate control, better codecs where the encoding gate allows — every byte saved under the rate gate converts to score (`comp = 1 − rate`, weighted 0.7 in the compression formula). - **Actual restoration**: deblur, denoise, tone/exposure correction, artifact repair before re-encoding. The challenge DAG degrades footage the way real pipelines do (capture → edit → delivery, [`vidaio/challenge/README.md`](../vidaio/challenge/README.md)); a miner that genuinely inverts those operators clears VMAF draws the reference miner honestly zeros on. This is the whole point of the subnet. - **Capacity + reliability**: no timeouts, no 429s at your typical load, exact digests — absent rounds decay your EWMA. What the gates make pointless: - **Metric gaming** — sharpening/contrast tricks that inflate default VMAF are precisely what the NEG-model delta gate hunts; tone/color/chroma manipulation has dedicated deterministic CPU gates. - **Replay/collusion** — cross-miner dedup zeroes copies; the task id is validator-authored and bound; the scored bytes are digest-pinned; the PieAPP sample frame derives from `sha256(content_digest || challenge_id)` — nothing is predictable before dispatch. - **Enhancement shortcuts on upscaling** — file-size caps and the pass/fail VMAF gate bound the space; PieAPP quality is what scores. - **Seed/DAG probing** — challenge parameters are private, committed (commit-reveal) BEFORE dispatch, and dispatch payloads are structurally leak-probed. **The integrity invariant**: every score is an exact, archived ItemScore packet — metrics, gate outcomes, canonicalization plan digest, scorer identity — and each epoch's weight vector is derived from an immutable **epoch log** that merkle-commits to those packets (root + per-item inclusion proofs) and is **anchored on chain**. Any third party can recompute any score from the audit store ([`vidaio/audit/README.md`](../vidaio/audit/README.md)), and the validators themselves do exactly that as **auditors**: each independently recomputes a sample over the real engine, RE-FOLDS the earning state, re-derives the weight vector, and POSTs a signed verdict to the **Audit Results API** ([`vidaio/auditor/README.md`](../vidaio/auditor/README.md), [`vidaio/audit_api/README.md`](../vidaio/audit_api/README.md)). A substituted score is `SCORE_MISMATCH`, a substituted weight is `WEIGHT_DERIVATION_MISMATCH`, a substituted earning state is `EARNING_STATE_MISMATCH`, and a caught substitution surfaces publicly as a **DISPUTED** epoch. There is no substitution path in this codebase, for you or against you; and the central scorer cannot cherry-pick corruptions after seeing your output, because the challenge commitment binds seed + DAG + asset + scorer before dispatch and the sampling is seeded by the finalized hash of the fixed future block `close_block + K`, which it cannot predict while building and anchoring the log. --- ## Mainnet The subnet is LIVE on mainnet **netuid 85** — register and mine today. `chain.mode: bittensor` is the production default and constructs the real `BittensorChainAdapter` (lazily importing the optional `.[chain]` bittensor deps — a missing dep fails fast with `NotConfiguredError` pointing at the extra). The real-chain path was exercised end to end on live testnet before launch — the substrate/archive path, both inference tracks, and both earning competition tracks. What follows is the miner-side operating contract. - **Registration is standard btcli, operator-side**: `btcli subnet register` with your hotkey on netuid 85 (`core.netuid`) — no code path registers for you, and coldkeys are never touched at runtime. - **Run the public miner least-privileged** from the release image with only its miner configuration, its own hotkey-only wallet, and media runtime; it does not need a validator wallet or S3 secrets: ```sh python scripts/service_entrypoint.py reference-miner ``` Set `miner.artifact_hotkey` equal to this role's `chain.validator_hotkey` and loaded miner wallet. Keep `miner.allow_unsigned_artifact_v1: false` and `miner.enable_legacy_path_routes: false`; production rejects both compatibility opt-ins. Put permissionless endpoints behind the hardened edge described above. - **Advertise the HTTP(S) endpoint after registration and startup.** Configure the same miner-specific `chain.validator_hotkey`/wallet environment used by the serving process, then publish and verify its globally routable literal address with the helper: ```sh python scripts/advertise_miner.py --config config/default.yaml \ --external-ip --external-port 8300 --external-scheme https ``` This submits the Bittensor `serve_axon` extrinsic only for metagraph discovery; the miner continues serving the streamed HTTP(S) protocol, not a Synapse/Axon application server. Axon data carries no scheme, so the argument must match every validator's `miner_url_scheme` — on mainnet that is `https`, verified against a publicly trusted certificate for the literal IP. A port that still speaks plain HTTP (a common leftover from the legacy axon) is skipped every round with `warrant probe failed` and earns nothing. The helper waits for finalization and exact IP/port readback. - **Endpoint discovery**: the adapter reads the metagraph axon IP and port. Production dials only globally routable literal IPs; unspecified addresses are not dialed (and are separately exempt from reward IP dedup). The real axon port is preserved/preferred, with `validator.miner_port` only a fallback. Validate serving and reachability before you expect scored rounds. - **Same signed wire contract**: artifact-v2 `POST /v1/task/artifact` + `GET /warrant`. A controlled reference fleet may additionally set `miner.api_token`; permissionless miners leave the global shared token unset and use hotkey auth plus the hardened public edge described above. No validator/miner shared filesystem is required. - **Deregistration/churn**: the designed reconciliation keys earnings by hotkey, resolves hotkey→uid fresh at weight time; a hotkey swap purges your EWMA history (you restart as a new miner). The same code path serves testnet and mainnet — only the `chain.*` endpoint and netuid differ — so you can rehearse your full setup (registration, advertisement, serving, scored rounds) on testnet before registering the mainnet hotkey.