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: GEO Score, Mention Rate, Source Rate, 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: {
      geoScore: performance.scores.global,
      mentionRate: performance.scores.mentionRate,
      sourceRate: performance.scores.sourceRate,
      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,
    },
  };
}

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);

Example Output

{
  "kpis": {
    "geoScore": 62,
    "mentionRate": 45.2,
    "sourceRate": 23.9,
    "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.geoScorenumberOverall GEO optimization score (0-100)
kpis.mentionRatenumber% of AI responses mentioning your brand
kpis.sourceRatenumber% of AI responses citing your domain
kpis.shareOfVoicenumberYour % of all competitor mentions
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,
    })),
  };
}
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