Loading
Loading
Sign a small file with your subnet owner coldkey to show that a repo, a website or another link is yours. bittensor.ai then marks those links as verified.
Anyone can type any URL into an on-chain identity or a README. A proof is a bittensor.json file that lists your links and is signed with your subnet owner coldkey. You publish it in your repo or on your website.
A link is verified only while its proof:
Proofs are checked about once a day. If a check fails (the file is gone, the owner coldkey changed, the proof expired), the link stops being verified.
The subnet's owner coldkey signed, within the last 90 days, that this link belongs to the subnet.
You need:
SubtensorModule.NetworkRegisteredAt(netuid). Write it without separators;Nothing here asks for your seed, mnemonic or password. You sign in your own wallet.
UTF-8 JSON, at most 16 KiB. One proof is an object with exactly these keys:
| Key | Type | Rule |
|---|---|---|
version | integer | 1 |
network | string | "finney" (mainnet). No other network is accepted. |
netuid | integer | 0 to 65535 |
network_registered_at | integer | the block your subnet's current registration started at |
coldkey | string | the subnet owner coldkey, an SS58 address starting with 5 |
links | array | 1 to 32 objects {"kind": …, "url": …}, with exactly these two keys |
issued_at | string | UTC, exactly YYYY-MM-DDTHH:MM:SSZ |
expires_at | string | the same format, after issued_at, at most 90 days after it |
signature | string | 0x and 128 hex digits: the coldkey's 64-byte sr25519 signature |
kind is one of repo, website, docs, blog, x, discord, telegram, huggingface, youtube, linkedin, other. It is your own label: links are matched by URL, not by kind.
Each link's URL:
https:// and has a host name: no IP address, no port, no user or password, no # fragment;xn-- form;token, access_token, signatures, X-Amz-*);Every text value is printable ASCII. Numbers are whole numbers. Duplicate keys make the file invalid.
An example, for netuid 12:
{
"version": 1,
"network": "finney",
"netuid": 12,
"network_registered_at": 4567890,
"coldkey": "5CcyqxXnJucaCnQQvvUg5EPzj1uoNAxACZvzArHw5aVDvgNH",
"links": [
{"kind": "repo", "url": "https://github.com/acme/subnet"},
{"kind": "website", "url": "https://acme.example/"},
{"kind": "x", "url": "https://x.com/acme"}
],
"issued_at": "2026-10-01T00:00:00Z",
"expires_at": "2026-12-30T00:00:00Z",
"signature": "0xc665bbe936848e767cc6035d4f116ed5d7569d9e2fc49e3fa3e9b3791dc3d24a51c707aa734b4a95d6e134324a32dc4b45e1b5bed135b949a69f544b64ab2388"
}This example is valid for exactly 90 days, the maximum. Its coldkey is a public test key: never send funds to it.
Own several subnets? Put up to 16 proofs in one file, at most one per subnet: {"proofs": [<proof>, …]}.
Either place, or both. Each is checked on its own.
bittensor.json at the root of the default branch of a repo linked to your subnet, such as the GitHub repo in its on-chain identity. Other branches and tags are not read.https://<host>/.well-known/bittensor.json, where <host> is the host of the website in your subnet's on-chain identity.www. twin (example.com and www.example.com), https on every hop.application/json (text/plain and application/octet-stream work too), at most 16 KiB, and let robots.txt allow it.The published file can be pretty-printed, with its keys in any order: the message is rebuilt from the values.
The coldkey signs one line of ASCII: bittensor-ownership-proof:v1: followed by the proof without its signature key, written as JSON this way:
, and : with no spaces, no line breaks;In Python that is json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True). The bytes signed are the message's ASCII bytes. Nothing is hashed first.
The message for the example above:
bittensor-ownership-proof:v1:{"coldkey":"5CcyqxXnJucaCnQQvvUg5EPzj1uoNAxACZvzArHw5aVDvgNH","expires_at":"2026-12-30T00:00:00Z","issued_at":"2026-10-01T00:00:00Z","links":[{"kind":"repo","url":"https://github.com/acme/subnet"},{"kind":"website","url":"https://acme.example/"},{"kind":"x","url":"https://x.com/acme"}],"netuid":12,"network":"finney","network_registered_at":4567890,"version":1}Its sha256 is 515296b85818a29e482d00906962978086a42888640585855f24e22025082d87. Build the same message from the example to check your own code.
Two forms of signature are accepted: over the message as it is, or over <Bytes> + message + </Bytes>, which is what wallet extensions sign. sr25519 signatures are randomised: signing twice gives two different signatures, both valid.
The builder is a single HTML file you download and open from your own computer. It makes no network calls and never asks for a key. Enter the netuid, the registration block, the coldkey address and your links, and choose a lifetime of 1 to 90 days. It checks the rules above, shows the message to sign and its hex, and writes bittensor.json once you paste the signature back.
Download the proof builder (ownership-proof.html)
Then sign the message with the subnet owner coldkey, in one of these ways.
--no-use-hotkey signs with the coldkey. Put the message between '<Bytes> and </Bytes>', in single quotes. For the example:
btcli wallet sign --wallet-name <owner wallet> --no-use-hotkey --json-output --message '<Bytes>bittensor-ownership-proof:v1:{"coldkey":"5CcyqxXnJucaCnQQvvUg5EPzj1uoNAxACZvzArHw5aVDvgNH","expires_at":"2026-12-30T00:00:00Z","issued_at":"2026-10-01T00:00:00Z","links":[{"kind":"repo","url":"https://github.com/acme/subnet"},{"kind":"website","url":"https://acme.example/"},{"kind":"x","url":"https://x.com/acme"}],"netuid":12,"network":"finney","network_registered_at":4567890,"version":1}</Bytes>'btcli asks for the coldkey password itself. Copy signed_message from its output as one line (the terminal may wrap it), with 0x in front. A link that contains a single quote can't be passed this way.
With bittensor-wallet or substrate-interface, which both have Keypair.sign:
import json
from bittensor_wallet import Wallet # or: from substrateinterface import Keypair
payload = {...} # every key of the proof except "signature"
message = "bittensor-ownership-proof:v1:" + json.dumps(
payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True
)
coldkey = Wallet(name="owner").coldkey # asks for the coldkey password
signature = "0x" + coldkey.sign(message.encode()).hex()
assert coldkey.verify(message.encode(), bytes.fromhex(signature[2:]))
with open("bittensor.json", "w") as f:
json.dump({**payload, "signature": signature}, f, indent=2)The extension signs <Bytes>message</Bytes>, which is accepted.
The builder can't check signatures, so check the file before you publish it. A file that does not check out must not be published. Either:
import json
from substrateinterface import Keypair # or: from bittensor_wallet import Keypair
doc = json.load(open("bittensor.json")) # a single proof (for a bundle, check each entry of "proofs")
signature = doc.pop("signature")
message = b"bittensor-ownership-proof:v1:" + json.dumps(
doc, sort_keys=True, separators=(",", ":"), ensure_ascii=True
).encode()
print(Keypair(ss58_address=doc["coldkey"]).verify(message, signature)) # must print TrueA signature from another key, a hotkey, or over a different or edited message (a trailing newline included) does not check out. Make sure the coldkey owns the subnet now (btcli subnet list, or the subnet's page).
network is finney; any other network is invalid (bad_network).expires_at is no more than 90 days after issued_at. Re-sign at least that often.network_registered_at), and only the current one is checked.Each place (repo, website) gets one result per check:
verifiednot_foundbittensor.json at the root of the repo's default branch, or serve it at https://<your site>/.well-known/bittensor.json.fetch_failedmalformedbad_network (the network is not finney) and not_yet_valid (issued_at is more than 5 minutes in the future).wrong_lifenetuid and network_registered_at, and sign a new proof.bad_signaturewrong_coldkeyexpiredexpires_at, at most every 90 days, and replace the file.