Bittensor Partner API
Real-time AI analysis of Bittensor subnets via SSE streaming. One endpoint, flexible sections.
Get Access — Partner License Key
To call this endpoint from your own service (outside /bittensor), you need a partner license key. Keys are issued through our Whop storefront — follow these three steps:
-
Apply for the partnership.
Visit whop.com/air-scope/bittensor-partnership and click the Apply button on the page. -
Wait for confirmation.
Want faster approval? Ping us on Discord right after you submit — mention your Whop username so we can match and approve your application on the spot. Once approved, the partnership product is added to your Whop account. -
Copy your license key.
Go to whop.com/joined/air-scope, open the Software tab, find the Bittensor Partnership entry, and copy the license key shown there. It looks like A-xxxxx-xxxxx-xxxxx.
Pass that key on every request as shown in Authentication below. Keep it secret — it identifies your account and counts against your quota.
Quota & Usage
Every partner key is issued with an initial trial allowance of 5 analyses. This is a one-time trial budget, not a recurring allowance — it's meant to let you evaluate the endpoint end-to-end (response shape, streaming behaviour, AI quality, rendering) before we agree on ongoing usage.
While using your trial quota:
- Spend the 5 calls deliberately — each call runs the full multi-section AI pipeline, which is non-trivial on our side.
- Keep notes as you go: what worked, what surprised you, fields you'd want, sections you'd cut, rendering glitches you hit, integration friction, etc.
- When you run out, send us those notes — thoughts, findings, improvement ideas, feature requests — on Discord.
Your current remaining quota is displayed directly inside the Analyze Subnet button on the live Bittensor page (format: remaining / limit).
Need more calls? Once the trial is done, reach out on Discord so we can talk through your use-case — expected volume, which sections you actually need, latency/cache tolerance, whether the output is rendered to end-users or consumed programmatically, etc. We'll then agree on a recurring quota and a matching payment plan that covers our compute/LLM/API costs on top of the value you're getting. Partner tiers are bespoke — there's no fixed price sheet, the quota and fee scale with what you actually need.
Quick Start — Test & Inspect
Once you have a license key, the fastest way to see the API in action is to use our live Bittensor page as a reference implementation. It calls this exact endpoint and renders the streaming output in real-time.
- Open /bittensor in your browser.
- Press F12 (or Cmd+Option+I) and select the Network tab.
- Enter a subnet identifier (e.g. 1, SN18, apex) and click Analyze Subnet.
- Find the request to /bittensor/api/v1/analysis/<netuid> and click it.
- Select the Response tab to watch raw SSE events arrive in real-time:
data: {"type": "metadata", "section": "subnet", "data": {...}}
data: {"type": "delta", "section": "subnet", "content": "The subnet shows "}
data: {"type": "delta", "section": "subnet", "content": "green(healthy decentralization)"}
data: {"type": "done", "section": "subnet", "full_text": "...", "duration": 12.3}
data: {"type": "complete"}
Tip: Toggle the page's Stream, Fresh, Sections, and Fields controls and re-run to see how each query param changes the response. The full copy-paste implementation is in the Code Example section below.
Endpoint
GET https://air-scope.ai/bittensor/api/v1/analysis/<netuid>Identifier: pass a netuid (1), SN-prefixed form (SN18), or a subnet name (apex). Case-insensitive — all variations resolve to the same subnet.
Authentication
Header: X-API-Key: A-xxxxx-xxxxx-xxxxx
Or Query: ?airscope_api_key=A-xxxxx-xxxxx-xxxxxQuery Parameters
| sections | Comma-separated sections to analyze. Default: all (subnet,tech,twitter,sentiment) Examples: ?sections=subnet ?sections=subnet,tech |
| fields | What data to include. Default: all analysis = AI-generated text only ( delta + done events)metadata = Raw collected data only ( metadata events, no AI)all = Everything (metadata + AI text) |
| fresh | Bypass cache and force a fresh analysis. Default: false Values: true, 1, yes |
| streaming | Return SSE stream or a single JSON response. Default: true true = Server-Sent Events (real-time) false = Wait for all sections, return one JSON |
Common Use Cases
GET /bittensor/api/v1/analysis/1
GET /bittensor/api/v1/analysis/apex?sections=subnet&fields=analysis
GET /bittensor/api/v1/analysis/18?sections=subnet,tech
GET /bittensor/api/v1/analysis/1?sections=tech&fields=metadata
GET /bittensor/api/v1/analysis/1?sections=twitter,sentiment&fresh=true
GET /bittensor/api/v1/analysis/1?streaming=false
SSE Event Types
progress | Status while fetching upstream data |
cached | Serving from DB cache (group_id, timestamp) |
metadata | Raw collected data — data.combined_data holds the section's full dataset. Filtered out by ?fields=analysis |
start | AI streaming started for a section |
delta | Incremental AI text chunk — content + section. Filtered out by ?fields=metadata |
done | Section complete — includes full_text, duration, or skipped: true |
error | Section-level error (non-fatal, other sections continue) |
complete | All requested sections finished |
Code Example — Build Your Own Dashboard
This is the exact pattern our Bittensor page uses. Copy-paste it into your dashboard to get streaming AI analysis with color markup rendered as you type. It does three things:
- Opens an SSE stream via
fetch()(works cross-origin, supports theX-API-Keyheader). - Incrementally parses each
deltaevent and convertsgreen(),red(), andsummary()markup into styled HTML spans as chunks arrive. - Writes the result to a DOM container in real-time, one section at a time.
1. HTML — container for each section
<style>
/* Markup classes — match what the AI emits */
.good { color: #3fb950; font-weight: 600; } /* green(text) */
.bad { color: #f85149; font-weight: 600; } /* red(text) */
.summary { color: #1f6feb; font-weight: 600; text-transform: uppercase; } /* summary(text) */
.section { background: #161b22; padding: 12px; margin-bottom: 12px; border-radius: 6px; }
.section h3 { color: #f0f6fc; margin: 0 0 8px 0; font-size: 15px; }
.section .body { color: #c9d1d9; line-height: 1.6; white-space: pre-wrap; }
</style>
<div id="subnet" class="section"><h3>Subnet Chain</h3><div class="body"></div></div>
<div id="tech" class="section"><h3>Tech</h3><div class="body"></div></div>
<div id="twitter" class="section"><h3>Twitter</h3><div class="body"></div></div>
<div id="sentiment" class="section"><h3>Sentiment</h3><div class="body"></div></div>2. StreamColorParser — converts markup tokens to HTML spans as chunks arrive
The AI emits inline tokens that must be translated into styled spans. A naive regex doesn't work here because a token like summary( can be split across two SSE chunks (e.g. "...sum" then "mary(..."). The parser below keeps a small buffer, detects the three supported markers, and handles nested parentheses inside the content.
- green(text) →
<span class="good">text</span>— positive / healthy findings - red(text) →
<span class="bad">text</span>— negative / risk findings - summary(text) →
<span class="summary">text</span>— inline section header / key callout (uppercase)
/**
* Incremental parser for `green(...)`, `red(...)`, `summary(...)` markup.
* Handles:
* - tokens split across SSE chunks (keeps up to N trailing chars in buffer)
* - nested parentheses inside the content (tracks `parenDepth`)
* - HTML escaping of the surrounding text
*
* Usage:
* const parser = new StreamColorParser();
* el.insertAdjacentHTML('beforeend', parser.processChunk(chunk)); // per delta
* el.insertAdjacentHTML('beforeend', parser.flush()); // on done
*/
class StreamColorParser {
constructor() {
this.buffer = '';
this.currentClass = null; // active span class, or null when outside a marker
this.parenDepth = 0;
}
// Tokens the AI emits → CSS class the parser wraps them in.
static PATTERNS = [
{ tag: 'green(', cls: 'good' },
{ tag: 'red(', cls: 'bad' },
{ tag: 'summary(', cls: 'summary' },
];
// Longest marker prefix we might see partially at the end of buffer.
// Used to decide how many trailing chars to hold back on each chunk.
static MAX_MARKER_LEN = 8; // 'summary(' is 8 chars — the longest token
processChunk(chunk) {
this.buffer += chunk;
let html = '';
while (this.buffer.length) {
const r = this.currentClass ? this._inside() : this._findStart();
html += r.html;
if (r.done) break;
}
return html;
}
// We're inside a colored region — emit chars until we hit the matching ')'.
_inside() {
let html = '', i = 0;
while (i < this.buffer.length) {
const c = this.buffer[i];
if (c === '(') {
this.parenDepth++;
html += this._esc(c); i++;
} else if (c === ')') {
if (this.parenDepth === 0) {
// End of marker — close the span and resume normal parsing.
html += '</span>';
this.currentClass = null;
this.buffer = this.buffer.slice(i + 1);
return { html, done: false };
}
this.parenDepth--;
html += this._esc(c); i++;
} else {
html += this._esc(c); i++;
}
}
this.buffer = '';
return { html, done: true };
}
// Looking for the next marker start; emit plain escaped text before it.
_findStart() {
const lower = this.buffer.toLowerCase();
let best = null, bestIdx = this.buffer.length;
for (const p of StreamColorParser.PATTERNS) {
const idx = lower.indexOf(p.tag);
if (idx !== -1 && idx < bestIdx) { best = p; bestIdx = idx; }
}
if (best) {
const html = this._esc(this.buffer.slice(0, bestIdx))
+ `<span class="${best.cls}">`;
this.currentClass = best.cls;
this.parenDepth = 0;
this.buffer = this.buffer.slice(bestIdx + best.tag.length);
return { html, done: false };
}
// No marker in view — emit everything except the last few chars,
// which might be the prefix of a marker split across the next chunk.
const safe = Math.max(0, this.buffer.length - StreamColorParser.MAX_MARKER_LEN);
const html = safe > 0 ? this._esc(this.buffer.slice(0, safe)) : '';
this.buffer = this.buffer.slice(safe);
return { html, done: true };
}
// Call when the stream ends — emits any remaining buffered text and
// closes an unterminated span (shouldn't happen with well-formed output).
flush() {
const html = this._esc(this.buffer) + (this.currentClass ? '</span>' : '');
this.buffer = ''; this.currentClass = null; this.parenDepth = 0;
return html;
}
_esc(t) {
return t.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
}
}3. streamAnalysis — fetch SSE, route events, write HTML as deltas arrive
One StreamColorParser instance per section keeps per-section buffers isolated.
On each delta event we call processChunk(content) and append the returned HTML;
on done we call flush() to emit any trailing text.
const API_KEY = 'A-xxxxx-xxxxx-xxxxx';
const BASE = 'https://air-scope.ai/bittensor/api/v1/analysis';
async function streamAnalysis(identifier, params = {}) {
const qs = new URLSearchParams(params).toString();
const url = `${BASE}/${encodeURIComponent(identifier)}${qs ? '?' + qs : ''}`;
const resp = await fetch(url, {
headers: { 'X-API-Key': API_KEY, 'Accept': 'text/event-stream' }
});
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);
const parsers = {}; // section -> StreamColorParser
const bodies = {}; // section -> DOM body element
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const getBody = (section) => {
if (!bodies[section]) {
const card = document.getElementById(section);
bodies[section] = card ? card.querySelector('.body') : null;
parsers[section] = new StreamColorParser();
}
return bodies[section];
};
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// SSE messages are separated by a blank line (\n\n).
let idx;
while ((idx = buffer.indexOf('\n\n')) !== -1) {
const msg = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
if (!msg.startsWith('data: ')) continue;
const event = JSON.parse(msg.slice(6));
const { type, section } = event;
if (type === 'delta' && section) {
// Incremental: parse chunk → colored HTML → append.
const body = getBody(section);
if (body) body.insertAdjacentHTML('beforeend',
parsers[section].processChunk(event.content || ''));
} else if (type === 'done' && section && parsers[section]) {
// Flush any trailing buffered text once the section finishes.
const body = getBody(section);
if (body) body.insertAdjacentHTML('beforeend', parsers[section].flush());
} else if (type === 'metadata' && section) {
// event.data.combined_data = raw section data (price, validators, ...)
console.log(`[${section}] metadata`, event.data);
} else if (type === 'error') {
console.error(`[${event.section || 'stream'}]`, event.message);
}
}
}
}
// Usage — identifier can be a netuid, SN-prefixed form, or subnet name.
streamAnalysis(1, { sections: 'subnet,tech,twitter,sentiment' });
streamAnalysis('SN18');
streamAnalysis('apex');4. Non-streaming JSON mode (no SSE parsing)
If you don't need real-time updates, pass streaming=false and get a single JSON response.
The AI text still contains green()/red()/summary() markup — you can reuse
StreamColorParser on the full string (one processChunk + one flush) to render it.
const resp = await fetch(
`${BASE}/1?streaming=false`,
{ headers: { 'X-API-Key': API_KEY } }
);
const data = await resp.json();
for (const [name, section] of Object.entries(data.sections)) {
if (section.analysis) {
const p = new StreamColorParser();
const html = p.processChunk(section.analysis) + p.flush();
document.querySelector(`#${name} .body`).innerHTML = html;
}
if (section.metadata) console.log(`[${name}] raw:`, section.metadata);
}Markup Tokens
StreamColorParser above translates them to styled spans;
if you render the raw text yourself, map them to the following CSS classes:
| green(text) | Positive / healthy finding → .good (color: #3fb950, bold) |
| red(text) | Negative / risk finding → .bad (color: #f85149, bold) |
| summary(text) | Inline section header / key callout → .summary (uppercase, bold) |
Example AI output:
summary(OVERVIEW) Subnet 1 shows green(healthy decentralization with ~45 active validators)
though red(incentive Gini at 0.72) suggests concentrated rewards.Errors
400 | Invalid section name, or ambiguous subnet name (multiple matches — response includes a matches array) |
401 | Missing API key |
403 | Invalid or inactive API key |
404 | Subnet not found |
429 | Quota exceeded (response includes limit, used, resets_at) |