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:

  1. Apply for the partnership.
    Visit whop.com/air-scope/bittensor-partnership and click the Apply button on the page.
  2. 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.
  3. 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.

  1. Open /bittensor in your browser.
  2. Press F12 (or Cmd+Option+I) and select the Network tab.
  3. Enter a subnet identifier (e.g. 1, SN18, apex) and click Analyze Subnet.
  4. Find the request to /bittensor/api/v1/analysis/<netuid> and click it.
  5. Select the Response tab to watch raw SSE events arrive in real-time:
data: {"type": "progress", "message": "Fetching subnet data..."}
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>
Sections: subnet, tech, twitter, sentiment
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-xxxxx
Query 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
Full analysis — all sections, all data (metadata + AI text)
GET /bittensor/api/v1/analysis/apex?sections=subnet&fields=analysis
Only subnet chain analysis, AI text only — lightest response for dashboards
GET /bittensor/api/v1/analysis/18?sections=subnet,tech
Subnet + tech analysis with both metadata and AI text
GET /bittensor/api/v1/analysis/1?sections=tech&fields=metadata
Raw tech data only (GitHub stats, README, website) — no AI, great for custom processing
GET /bittensor/api/v1/analysis/1?sections=twitter,sentiment&fresh=true
Fresh Twitter + sentiment analysis, skip cache
GET /bittensor/api/v1/analysis/1?streaming=false
Non-streaming — single JSON response with all sections
SSE Event Types
Each message arrives as data: {"type": "...", ...}
progressStatus while fetching upstream data
cachedServing from DB cache (group_id, timestamp)
metadataRaw collected data — data.combined_data holds the section's full dataset. Filtered out by ?fields=analysis
startAI streaming started for a section
deltaIncremental AI text chunk — content + section. Filtered out by ?fields=metadata
doneSection complete — includes full_text, duration, or skipped: true
errorSection-level error (non-fatal, other sections continue)
completeAll 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:

  1. Opens an SSE stream via fetch() (works cross-origin, supports the X-API-Key header).
  2. Incrementally parses each delta event and converts green(), red(), and summary() markup into styled HTML spans as chunks arrive.
  3. 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
    }
}
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
AI responses use three inline tokens. The 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
400Invalid section name, or ambiguous subnet name (multiple matches — response includes a matches array)
401Missing API key
403Invalid or inactive API key
404Subnet not found
429Quota exceeded (response includes limit, used, resets_at)