Skip to main content
Build a custom dashboard displaying your brand’s GEO performance metrics, competitor rankings, and top sources.
This guide uses the API client from the Guides index. Copy it to your project first.

What You’ll Build

A dashboard with:
  • KPI cards: Mention Rate, Source Rate, Coverage, Share of Voice, Sentiment
  • Competitor ranking: Share of Voice comparison with gap analysis
  • Source ranking: Share of Citations by domain

Fetch Dashboard Data

Fetch all data in parallel for optimal performance.
async function fetchDashboardData(client, brandId, period = 30) {
  // Parallel API calls
  const [performance, competitors, sources] = await Promise.all([
    client.getPerformance(brandId, { period }),
    client.getCompetitors(brandId, { period, limit: 10, sort: 'shareOfVoice', order: 'desc' }),
    client.getSourceDomains(brandId, { period, limit: 10, sort: 'mentions', order: 'desc' }),
  ]);

  // Find your brand in competitors list
  const yourBrand = competitors.competitors.find(c => c.relationship === 'SELF');
  const directCompetitors = competitors.competitors.filter(c => c.relationship === 'DIRECT');

  return {
    kpis: {
      mentionRate: performance.scores.mentionRate,
      sourceRate: performance.scores.sourceRate,
      coverage: performance.scores.coverage,
      shareOfVoice: performance.scores.shareOfVoice,
      sentiment: performance.scores.sentiment,
    },

    yourBrand: yourBrand ? {
      name: yourBrand.name,
      mentions: yourBrand.totalMentions,
      shareOfVoice: yourBrand.shareOfVoice,
      avgPosition: yourBrand.avgPosition,
      sentiment: yourBrand.avgSentiment,
    } : null,

    competitorRanking: directCompetitors.map(c => ({
      name: c.name,
      shareOfVoice: c.shareOfVoice,
      mentions: c.totalMentions,
      gap: yourBrand ? (c.shareOfVoice - yourBrand.shareOfVoice).toFixed(2) : null,
    })),

    sourceRanking: sources.sources.map(s => ({
      domain: s.domain,
      shareOfCitations: s.rate,
      mentions: s.totalMentions,
      isSelf: s.isSelf,
    })),

    methodology: {
      period: `${period} days`,
      promptsAnalyzed: performance.methodology.promptsCount,
      responsesGenerated: performance.methodology.responsesTotal,
      providers: performance.methodology.providers,
    },
  };
}
from dataclasses import dataclass
from typing import List, Optional
from concurrent.futures import ThreadPoolExecutor


@dataclass
class DashboardData:
    kpis: dict
    your_brand: Optional[dict]
    competitor_ranking: List[dict]
    source_ranking: List[dict]
    methodology: dict


def fetch_dashboard_data(client, brand_id: str, period: int = 30) -> DashboardData:
    """Fetch all dashboard data in parallel."""

    with ThreadPoolExecutor(max_workers=3) as executor:
        perf_future = executor.submit(client.get_performance, brand_id, period=period)
        comp_future = executor.submit(client.get_competitors, brand_id, period=period, limit=10, sort='shareOfVoice')
        src_future = executor.submit(client.get_source_domains, brand_id, period=period, limit=10, sort='mentions')

        performance = perf_future.result()
        competitors = comp_future.result()
        sources = src_future.result()

    your_brand = next((c for c in competitors['competitors'] if c['relationship'] == 'SELF'), None)
    direct_competitors = [c for c in competitors['competitors'] if c['relationship'] == 'DIRECT']

    return DashboardData(
        kpis={
            'mention_rate': performance['scores']['mentionRate'],
            'source_rate': performance['scores']['sourceRate'],
            'coverage': performance['scores']['coverage'],
            'share_of_voice': performance['scores']['shareOfVoice'],
            'sentiment': performance['scores']['sentiment'],
        },
        your_brand={
            'name': your_brand['name'],
            'mentions': your_brand['totalMentions'],
            'share_of_voice': your_brand['shareOfVoice'],
            'avg_position': your_brand['avgPosition'],
            'sentiment': your_brand['avgSentiment'],
        } if your_brand else None,
        competitor_ranking=[
            {
                'name': c['name'],
                'share_of_voice': c['shareOfVoice'],
                'mentions': c['totalMentions'],
                'gap': round(c['shareOfVoice'] - your_brand['shareOfVoice'], 2) if your_brand else None,
            }
            for c in direct_competitors
        ],
        source_ranking=[
            {
                'domain': s['domain'],
                'share_of_citations': s['rate'],
                'mentions': s['totalMentions'],
                'is_self': s['isSelf'],
            }
            for s in sources['sources']
        ],
        methodology={
            'period': f"{period} days",
            'prompts_analyzed': performance['methodology']['promptsCount'],
            'responses_generated': performance['methodology']['responsesTotal'],
            'providers': performance['methodology']['providers'],
        },
    )

Usage

const client = new QwairyClient(process.env.QWAIRY_API_TOKEN);
const dashboard = await fetchDashboardData(client, 'your-brand-id', 30);

console.log('KPIs:', dashboard.kpis);
console.log('Your position:', dashboard.yourBrand);
console.log('Top competitors:', dashboard.competitorRanking);
console.log('Top sources:', dashboard.sourceRanking);
client = QwairyClient()
dashboard = fetch_dashboard_data(client, 'your-brand-id', period=30)

print(f"Mention Rate: {dashboard.kpis['mention_rate']}%")
print(f"Your Share of Voice: {dashboard.your_brand['share_of_voice']}%")
print(f"Top competitor gap: {dashboard.competitor_ranking[0]['gap']}%")

Example Output

{
  "kpis": {
    "mentionRate": 45.2,
    "sourceRate": 23.9,
    "coverage": 33.33,
    "shareOfVoice": 8.13,
    "sentiment": 78.1
  },
  "yourBrand": {
    "name": "My Brand",
    "mentions": 104,
    "shareOfVoice": 8.13,
    "avgPosition": 2.1,
    "sentiment": 78.1
  },
  "competitorRanking": [
    { "name": "Competitor A", "shareOfVoice": 12.5, "mentions": 156, "gap": "4.37" },
    { "name": "Competitor B", "shareOfVoice": 9.8, "mentions": 122, "gap": "1.67" }
  ],
  "sourceRanking": [
    { "domain": "industry-news.com", "shareOfCitations": 5.10, "mentions": 102, "isSelf": false },
    { "domain": "mybrand.com", "shareOfCitations": 2.25, "mentions": 45, "isSelf": true }
  ],
  "methodology": {
    "period": "30 days",
    "promptsAnalyzed": 156,
    "responsesGenerated": 312,
    "providers": ["chatgpt", "perplexity"]
  }
}

Data Structure Reference

FieldTypeDescription
kpis.mentionRatenumber% of brand-mentioning AI responses that mention yours
kpis.sourceRatenumber% of source-citing AI responses that cite your domain
kpis.coveragenumber% of all monitored AI responses mentioning your brand
kpis.shareOfVoicenumberYour % of all brand mentions (SELF + DIRECT)
kpis.sentimentnumberAverage sentiment score (0-100)
competitorRanking[].gapstringDifference in Share of Voice vs your brand
sourceRanking[].shareOfCitationsnumberSource’s % of all citations (rate field)
sourceRanking[].isSelfbooleanWhether this is your own domain

LLM Diagnostics (Optional)

Go beyond aggregate scores: identify responses where your brand is mentioned but not cited (missed link opportunities), and discover what web queries AI models trigger before answering.
async function fetchDiagnostics(client, brandId, period = 30) {
  const [mentionedNotCited, searchQueries] = await Promise.all([
    client.getAnswers(brandId, {
      period,
      hasSelfMention: true,
      hasSelfSource: false,
      limit: 5,
      sort: 'createdAt',
      order: 'desc',
    }),
    client.getSearch(brandId, { period, limit: 5, sort: 'createdAt', order: 'desc' }),
  ]);

  return {
    // Responses where AI mentions you but doesn't link to your content
    missedCitations: mentionedNotCited.answers.map(a => ({
      prompt: a.promptText,
      provider: a.provider,
      position: a.selfMentionPosition,
      competitors: a.competitorsCount,
    })),
    // Web queries AI models run before generating a response
    aiSearchQueries: searchQueries.searches.map(s => ({
      query: s.query,
      prompt: s.prompt,
      provider: s.provider,
    })),
  };
}
def fetch_diagnostics(client, brand_id: str, period: int = 30) -> dict:
    """Fetch LLM diagnostic data."""
    from concurrent.futures import ThreadPoolExecutor

    with ThreadPoolExecutor(max_workers=2) as executor:
        mention_future = executor.submit(
            client.get_answers, brand_id, period=period,
            has_self_mention=True, has_self_source=False, limit=5,
        )
        search_future = executor.submit(
            client.get_search, brand_id, period=period, limit=5,
        )

        mentioned_not_cited = mention_future.result()
        search_queries = search_future.result()

    return {
        'missed_citations': [
            {
                'prompt': a['promptText'],
                'provider': a['provider'],
                'position': a.get('selfMentionPosition'),
                'competitors': a['competitorsCount'],
            }
            for a in mentioned_not_cited['answers']
        ],
        'ai_search_queries': [
            {
                'query': s['query'],
                'prompt': s['prompt'],
                'provider': s.get('provider'),
            }
            for s in search_queries['searches']
        ],
    }
Missed citations reveal where AI talks about you but doesn’t link to your content — your highest-ROI content opportunities. AI search queries show what AI models actually search for before responding, giving you direct content targeting signals.

Next Steps