# BittensorMCP > Live Bittensor chain access as MCP tools — free reads, premium on-chain writes. > One HTTPS endpoint, wallet-signature auth, self-custody: private keys never touch the server. This file is the complete agent onboarding. An agent with nothing but this document can go from zero (no wallet) to a working premium MCP session. Read top to bottom. ## Agent onboarding — zero to premium (START HERE) **Decision first — does your user already have a funded Bittensor wallet?** - **User has a wallet and wants to keep the keys** (most common): send them to https://bittensormcp.com/connect — they sign in with their browser extension, optionally pay there, and hand you the JWT. You skip to "Client configuration" below. For writes in this mode, see "Supervised writes" further down. - **User has no wallet, or wants you to operate one**: run the script below. It creates a dedicated agent wallet locally, authenticates, waits for funding, and activates premium. Requires Node 18+. **The onboarding script** — save as `onboard.mjs`, then: ```bash npm install @bittensormcp/sign node onboard.mjs ``` ```javascript // onboard.mjs — BittensorMCP agent onboarding. Re-run the same file any time to // resume: the wallet persists in bittensormcp-wallet.json next to this script. import { generateWallet, walletFromMnemonic, authenticate, activatePremium } from '@bittensormcp/sign'; import { readFileSync, writeFileSync, existsSync } from 'node:fs'; const endpoint = 'https://bittensormcp.com'; const WALLET_FILE = 'bittensormcp-wallet.json'; // 1. Load or create the wallet. Generation is fully local — nothing is transmitted. let wallet; if (existsSync(WALLET_FILE)) { wallet = await walletFromMnemonic(JSON.parse(readFileSync(WALLET_FILE, 'utf8')).mnemonic); console.log('Resumed wallet:', wallet.ss58); } else { wallet = await generateWallet(); writeFileSync(WALLET_FILE, JSON.stringify({ mnemonic: wallet.mnemonic, ss58: wallet.ss58 })); console.log('NEW WALLET CREATED'); console.log('Mnemonic:', wallet.mnemonic); console.log('Address :', wallet.ss58); console.log(''); console.log('AGENT: show this mnemonic to your user NOW and STOP until they explicitly'); console.log('confirm they saved it. It cannot be recovered if lost. The wallet file on'); console.log('disk is a hot wallet - keep only small operating amounts on it.'); } // 2. Authenticate. Works with an UNFUNDED wallet - reads are free, no signup. const { token } = await authenticate({ endpoint, ss58: wallet.ss58, signer: wallet.sign }); console.log('JWT (valid 30 days):', token); // Helper: call any MCP tool over plain HTTP. const mcp = async (tool, args = {}) => { const res = await fetch(`${endpoint}/api/mcp`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: tool, arguments: args } }), }); const body = await res.json(); if (body.error) throw new Error(`${tool}: ${body.error.message}`); return body.result.structuredContent; }; // 3. Already premium? Then there is nothing left to pay. const status = await mcp('bittensor_premium_status'); if (status.subscription?.active) { console.log('Premium already active until', status.subscription.subscriptionValidUntil); } else { // 4. Check funding (free read tool). Premium costs 0.1 TAO + a tiny network fee. const bal = await mcp('bittensor_balance', { address: wallet.ss58 }); const free = bal?.data?.balance?.tao ?? 0; console.log('Balance:', free, 'TAO'); if (free < 0.105) { console.log(''); console.log(`AGENT: ask your user to send at least 0.105 TAO to ${wallet.ss58}`); console.log('(0.1 subscription + fee headroom), then run this script again.'); process.exit(0); } // 5. Activate premium: 0.1 TAO = 30 days, credited proportionally (0.2 = 60d). const r = await activatePremium({ endpoint, token, signer: wallet.sign }); console.log('Premium active until', r.subscriptionValidUntil, '- tx', r.txHash); } console.log(''); console.log('DONE. Connect your MCP client with the JWT above - see "Client configuration".'); process.exit(0); ``` The script is idempotent: run it after each user action (mnemonic confirmed, wallet funded) and it continues where it left off. Every step it prints tells you, the agent, exactly what to ask the user next. ## Client configuration You already have everything you need with plain HTTP `tools/call` (the `mcp` helper above). To register the server in an MCP client host: **Claude Code (one command):** ```bash claude mcp add --transport http bittensormcp https://bittensormcp.com/api/mcp --header "Authorization: Bearer YOUR_WALLET_JWT" ``` **Claude Desktop / Cursor / any native Streamable-HTTP client:** ```json { "mcpServers": { "bittensormcp": { "url": "https://bittensormcp.com/api/mcp", "headers": { "Authorization": "Bearer YOUR_WALLET_JWT" } } } } ``` **Clients without native Streamable HTTP (mcp-remote bridge):** ```json { "mcpServers": { "bittensormcp": { "command": "npx", "args": [ "mcp-remote", "https://bittensormcp.com/api/mcp", "--header", "Authorization:${AUTH_HEADER}" ], "env": { "AUTH_HEADER": "Bearer YOUR_WALLET_JWT" } } } } ``` Note: no space after `Authorization:` — some clients split args on spaces, the env indirection is the documented mcp-remote workaround. Verified with mcp-remote 0.1.38. ## MCP endpoint ``` https://bittensormcp.com/api/mcp ``` Auth: `Authorization: Bearer ` · Transport: MCP Streamable HTTP (POST) · Protocol version: `2025-11-25`. All tool responses include `structuredContent` (typed object) alongside the text content. GET is not supported (405) — no SSE stream. ## What you can do **Free (no subscription)** — 13 read tools + premium_status check: - bittensor_balance — Free TAO balance of any SS58/coldkey address - bittensor_balance_batch — Free TAO balance for 2-50 SS58 addresses in one call - bittensor_account — Account overview: free balance + staked positions per subnet - bittensor_portfolio — Portfolio valued at current alpha prices - bittensor_subnet_list — Subnets with price, pools, emission, volume sorted by price; optional `limit` (1–256) for top-N - bittensor_subnet_metagraph — Top neurons by stake for a subnet (now includes per-neuron `active` and `last_update_block`) - bittensor_subnet_hyperparams — Subnet hyperparameters (tempo, kappa, burn…) - bittensor_subnet_cost — Current TAO cost to register a new subnet - bittensor_subnet_directory — Read a subnet's on-chain identity (name, URL, GitHub, Discord…); pass `netuid` for one, omit for a listing - bittensor_validators — Validators ranked by yield_per_alpha (neutral on-chain metric) - bittensor_dynamic — Alpha price, AMM pools, moving price, volume for a subnet - bittensor_swap_sim — Simulate TAO↔alpha swap: expected received amount + slippage - bittensor_block — Current block height and hash - bittensor_premium_status — Check tier, subscription state, treasury address, and next steps **Premium (TAO subscription, 0.1 TAO/month)** — all free tools plus 15 write tools and 1 premium read tool: Write tools carry MCP `annotations` in `tools/list`. Clients that respect annotations gate destructive tools behind a confirmation prompt before executing. Value-moving tools are tagged `destructiveHint: true, idempotentHint: false` — each call moves real TAO, replays are unsafe. State-overwrite tools are tagged `destructiveHint: false, idempotentHint: true` — calling twice with identical args is harmless. Free read tools carry `readOnlyHint: true, openWorldHint: true`. Pass `limit` (1–256) to `bittensor_subnet_list` on slow connections to avoid large payloads. - bittensor_stake_add — [destructive] Stake TAO on a subnet (auto-picks best validator or pass explicit hotkey) - bittensor_stake_remove — [destructive] Unstake alpha back to TAO (hotkey required — the validator your stake is with) - bittensor_stake_move — [destructive] Move stake between subnets without TAO conversion (hotkey required) - bittensor_transfer — [destructive] Transfer TAO from your coldkey to any SS58 address - bittensor_register — [destructive] Register a neuron via burned registration (hotkey required — the neuron key to register) - bittensor_serve_axon — [idempotent] Set axon endpoint (IP+port) — sign with HOTKEY session - bittensor_weights_set — [idempotent] Set weights as a validator — sign with HOTKEY session - bittensor_subnet_register — [destructive] Register a new subnet (hotkey required, becomes UID 0 of the new subnet; check bittensor_subnet_cost first, cost is dynamic) - bittensor_subnet_start_call — [destructive] Activate emissions on a subnet you own (netuid required; a registered subnet is dormant until this runs) - bittensor_subnet_identity_set — [idempotent] Set subnet branding: name, description, GitHub, contact, URL, logo, Discord (netuid required, identity fields optional) - bittensor_delegate_take_increase — [destructive] Raise your validator's delegate take (commission), 0-100 percent - bittensor_delegate_take_decrease — [destructive] Lower your validator's delegate take (commission), 0-100 percent - bittensor_children_set — [idempotent] Split a hotkey's stake/authority across child hotkeys by proportion (replaces the full child list each call) - bittensor_root_register — [destructive] Register a hotkey on the root network (netuid 0) - bittensor_root_claim — [destructive] Claim accumulated root-network emissions for one or more subnets Note: serve_axon and weights_* extrinsics are signed by the hotkey on-chain. Authenticate your session with the hotkey (any sr25519 key can open a session) to use them. All other write tools are signed by the coldkey session. **Premium read tool (no signing, no chain call):** - bittensor_activity_log — Your own write history (tool, args, txHash, timestamp), paginated newest-first. Pass `page`/`pageSize` (default 1/25, max pageSize 100). ### Worked example: register, activate, and brand a new subnet ``` 1. bittensor_subnet_cost {} → check the current lock cost 2. bittensor_subnet_register { "hotkey": "" } → UNSIGNED_PAYLOAD, sign, submit → returns { txHash, netuid } (netuid may be null if the event couldn't be decoded — if so, call bittensor_subnet_list and find your subnet by owner) 3. bittensor_subnet_start_call { "netuid": } → activates emissions 4. bittensor_subnet_identity_set { "netuid": , "subnet_name": "...", ... } → optional branding ``` Each step is a normal write tool call: you get `UNSIGNED_PAYLOAD`, sign the payload with your coldkey, then call `bittensor_submit_signed`. See "A2 signing" below for the exact mechanics. ### Worked example: validator commission, child-key delegation, root network ``` bittensor_delegate_take_increase { "hotkey": "", "take": 12 } → raises take to 12% bittensor_children_set { "hotkey": "", "netuid": 3, "children": [[0.5, ""], [0.5, ""]] } bittensor_root_register { "hotkey": "" } bittensor_root_claim { "subnets": [1, 3, 7] } ``` These four are independent of each other and of subnet registration — call any of them at any time on an existing validator hotkey. ## Authenticating (wallet-JWT flow, reference) The SDK `authenticate()` does all of this. Raw flow for non-Node environments: **Step 1 — request a challenge nonce:** ``` POST https://bittensormcp.com/api/auth/challenge Content-Type: application/json {"ss58": "5YourColdkeySs58Address", "domain": "bittensormcp.com"} ``` Response: `{"nonce": "<32-byte hex>"}` — valid 120 seconds, single use. **Step 2 — sign the challenge message** `bittensormcp-auth::bittensormcp.com`: ```python from substrateinterface import Keypair kp = Keypair.create_from_mnemonic("your mnemonic") signature = "0x" + kp.sign(f"bittensormcp-auth:{nonce}:bittensormcp.com").hex() ``` Or with btcli: ``` btcli wallet sign --wallet.name --message "bittensormcp-auth::bittensormcp.com" ``` **Step 3 — verify and get JWT:** ``` POST https://bittensormcp.com/api/auth/verify Content-Type: application/json {"ss58": "5...", "nonce": "", "signature": "0x..."} ``` Response: `{"token": "", "subscriptionValidUntil": null}` — JWT valid 30 days. Authentication works with an unfunded wallet; funds are only needed for premium/writes. Browser flow for humans: https://bittensormcp.com/connect (Polkadot.js, Talisman, SubWallet — or manual btcli signing at /connect/manual). ## OAuth (for connector-only clients, reference) Most clients (Claude Desktop, Cursor) use the direct bearer-token flow above — paste the JWT into your MCP config, done. Use OAuth instead only if your client's "Add connector" UI has no field for a static token and only offers an OAuth login. Both paths issue the exact same JWT and grant identical access — pick whichever your client supports. Discovery: `GET /.well-known/oauth-protected-resource` and `GET /.well-known/oauth-authorization-server` (RFC 9728 / RFC 8414) — spec-compliant clients find everything below automatically; you shouldn't need to hardcode these URLs. **Step 1 — register (RFC 7591, public client, no secret):** ``` POST https://bittensormcp.com/oauth/register Content-Type: application/json {"redirect_uris": ["https://your-client.example/callback"], "client_name": "My Agent"} ``` Response: `{"client_id": "...", "redirect_uris": [...], "token_endpoint_auth_method": "none"}` **Step 2 — authorize (PKCE S256 required):** generate a `code_verifier` (random string) and its `code_challenge` (`BASE64URL(SHA256(code_verifier))`), then send the user's browser to: ``` GET https://bittensormcp.com/oauth/authorize ?client_id= &redirect_uri= &response_type=code &code_challenge= &code_challenge_method=S256 &state= ``` This redirects to the normal wallet-signature UI at `/connect` — the user signs with their coldkey exactly as in the direct flow. On success, the browser is redirected to your `redirect_uri?code=&state=`. The code is single-use, ~60s TTL. **Step 3 — exchange the code:** ``` POST https://bittensormcp.com/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=&code_verifier= &redirect_uri=&client_id= ``` Response: `{"access_token": "", "token_type": "Bearer", "expires_in": 2592000}` — this `access_token` is the same JWT format `/api/auth/verify` issues; use it exactly the same way (`Authorization: Bearer ` on `/api/mcp`). No refresh token — re-run the flow after 30 days, same as the direct path. ## A2 signing (write tools, reference) Write tools return an `UNSIGNED_PAYLOAD` instead of submitting on-chain: ```json { "signal": "UNSIGNED_PAYLOAD", "intent_id": "", "payload": "", "expires_at": "", "hint": "Sign payload (sr25519) with your coldkey and call bittensor_submit_signed" } ``` Your agent must: (1) decode `payload` from hex to BYTES, (2) sign the bytes with sr25519 using the session key, (3) call `bittensor_submit_signed { intent_id, signature }`. The intent expires in 120 seconds — if expired, call the write tool again. **TypeScript SDK (handles all three steps):** ```typescript // npm install @bittensormcp/sign import { signAndSubmit } from '@bittensormcp/sign'; const result = await signAndSubmit({ endpoint, token: jwt, tool, args, signer: wallet.sign }); ``` **Python (self-contained — no package needed, only substrate-interface):** ```python # pip install substrate-interface import json, urllib.request from substrateinterface import Keypair def mcp_call(endpoint, jwt, tool, args): req = urllib.request.Request(endpoint + "/api/mcp", data=json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": {"name": tool, "arguments": args}}).encode(), headers={"Content-Type": "application/json", "Authorization": f"Bearer {jwt}"}) return json.load(urllib.request.urlopen(req))["result"]["structuredContent"] kp = Keypair.create_from_mnemonic("your mnemonic") step1 = mcp_call(endpoint, jwt, "bittensor_stake_add", {"netuid": 21, "amount": 0.01}) sig = "0x" + kp.sign(bytes.fromhex(step1["payload"].removeprefix("0x"))).hex() result = mcp_call(endpoint, jwt, "bittensor_submit_signed", {"intent_id": step1["intent_id"], "signature": sig}) print(result["txHash"]) ``` ## Billing (reference) The onboarding script's `activatePremium()` uses these endpoints — no premium gate on them (they are how you GET premium): **Step 1 — unsigned Transfer extrinsic to the treasury:** ``` POST https://bittensormcp.com/api/billing/sign-transfer Authorization: Bearer { "amountTao": 0.1 } → { "signal": "BILLING_UNSIGNED_PAYLOAD", "intent_id": "...", "payload": "", "expires_at": "..." } ``` **Step 2 — sign the payload bytes locally, submit:** ``` POST https://bittensormcp.com/api/billing/submit-signed Authorization: Bearer { "intent_id": "...", "signature": "0x" } → { "subscriptionValidUntil": "...", "creditedDays": 30, "txHash": "0x..." } ``` Credited proportionally: 0.2 TAO = 60 days, 0.15 TAO = 45 days. The billing intent expires in 120 seconds. After it returns, write tools are immediately available in the same session. **Manual fallback** (paid from any external wallet): send TAO to the treasury address (from `bittensor_premium_status`) FROM THE SESSION COLDKEY, then `POST /api/billing/confirm { "txHash": "0x..." }`. Sender must match the session ss58. ## Supervised writes (user keeps the keys) If the user authenticated via /connect and only gave you the JWT, you can still do writes without ever seeing a key: 1. Call the write tool → receive `UNSIGNED_PAYLOAD` with `intent_id` and `payload` 2. Give the `payload` hex to your user with this snippet to run locally: ```python from substrateinterface import Keypair kp = Keypair.create_from_mnemonic("USER'S MNEMONIC - never share with the agent") print("0x" + kp.sign(bytes.fromhex("PAYLOAD_HEX")).hex()) ``` 3. User pastes the signature back → call `bittensor_submit_signed { intent_id, signature }` 4. Mind the 120s intent TTL — if the user is slower, just call the write tool again ## Common pitfalls (read before debugging) - **Windows/PowerShell curl mangles JSON quotes** → server answers `400 Invalid JSON`. Don't inline JSON in PowerShell curl. Write the body to a file and use `curl.exe -d @body.json`, or use the Node/Python snippets instead. - **Sign the payload BYTES, not the hex string.** `payload` is hex — decode it first (`hexToBytes` / `bytes.fromhex`). Signing the ASCII hex string gives INVALID_SIGNATURE. - **Nonces and intents expire in 120 seconds.** NONCE_EXPIRED / INTENT_EXPIRED / BILLING_INTENT_EXPIRED just mean: request a fresh one and retry. - **Same key for session and signing.** The signature on submit is verified against the ss58 you authenticated as. Sign with a different key → INTENT_SS58_MISMATCH. - **`tools/list` shows 14 tools on a free account, 30 with an active premium subscription.** The 15 write tools plus bittensor_activity_log appear only once premium is active. Not a bug — activate premium first. - **GET /api/mcp returns 405.** Use POST (Streamable HTTP without SSE stream). - **A libuv assertion after the final output on Windows** (`Assertion failed: !(handle->flags & UV_HANDLE_CLOSING)`) is a harmless WASM teardown artifact of the crypto library at process exit. If all expected output printed before it, the run succeeded. ## Errors - `UNAUTHORIZED` — Missing or invalid JWT. Re-authenticate (or run the onboarding script). - `RATE_LIMITED` — Slow down; reads: 60/60s, writes: 10/60s. - `SUBSCRIPTION_EXPIRED` — Premium inactive. Activate via billing (see above). - `WRITE_NOT_AVAILABLE` — Tool needs premium; free session. Activate via billing. - `INTENT_EXPIRED` — Signing intent timed out (120s); call the write tool again. - `INVALID_SIGNATURE` — You signed the wrong bytes or with the wrong key (see pitfalls). - `UPSTREAM_UNAVAILABLE` — Bittensor node unreachable; retry later. ## Safety notes - Private keys never leave the agent/user process — the server only sees signatures - Write tools are neutral: no strategy imposed, no position limits beyond chain rules - Session JWT never appears in URL paths and is never forwarded to downstream services - Agent-operated wallets are hot wallets: keep only small operating amounts on them ## Contact / source - Web: https://bittensormcp.com - Human sign-in: https://bittensormcp.com/connect - Signing SDK (npm): https://www.npmjs.com/package/@bittensormcp/sign - Python: no package needed — self-contained snippets above (substrate-interface only)