Skip to main content

Overview

Argument Cartographer performs multiple API calls and AI operations per analysis. This guide helps you optimize performance and reduce costs based on the actual implementation.

Analysis Performance

Current Performance Characteristics

A typical argument analysis involves:
1

Search Query Generation

1 AI call to generate optimized search query
  • Model: Gemini 1.5 Pro
  • Tokens: ~100 input, ~20 output
  • Duration: 1-2 seconds
2

Web Search

1-2 Firecrawl search calls for diverse sources
  • Results: Up to 20 URLs per search
  • Duration: 2-3 seconds per search
  • Cost: ~$0.01 per search (varies by plan)
3

Web Scraping

8 parallel Firecrawl scrape calls for article content
// From generate-argument-blueprint.ts:210
if (urlsToScrape.length >= 8) break; // Aim for 8 diverse sources
  • Duration: 5-10 seconds (parallel execution)
  • Data: 20,000 chars per source (160KB total)
  • Cost: ~$0.08 per analysis
4

Main AI Analysis

1 large AI call with full context
  • Model: Gemini 1.5 Pro
  • Input: 8 sources × 12,000 chars = ~96,000 chars (~24,000 tokens)
  • Output: Structured JSON blueprint (~2,000 tokens)
  • Duration: 10-20 seconds
  • Cost: ~$0.01 per analysis
5

Twitter Search

1 Twitter API call for social pulse (optional)
  • Results: 20 tweets
  • Duration: 1-2 seconds
  • Cost: Free (within rate limits)
6

Social Pulse Generation

1 AI call to summarize tweets
  • Tokens: ~1,000 input, ~200 output
  • Duration: 2-3 seconds
Total per analysis:
  • Duration: 20-35 seconds
  • API calls: 3 AI + 10 Firecrawl + 1 Twitter
  • Estimated cost: $0.10-0.15 per analysis

Optimization Strategies

Reduce Number of Sources

Current: 8 diverse sources scraped per analysis Optimization: Reduce to 5 sources for faster, cheaper analyses:
// In generate-argument-blueprint.ts:208
if (urlsToScrape.length >= 5) break; // Reduced from 8
Impact:
  • ⚡ 30% faster scraping
  • 💰 40% lower Firecrawl costs
  • ⚠️ Slightly less comprehensive analysis

Reduce Content Length Per Source

Current: 12,000 chars per source in context
// From generate-argument-blueprint.ts:231
${doc.content.substring(0, 12000)}
Optimization: Reduce to 8,000 chars:
${doc.content.substring(0, 8000)}
Impact:
  • ⚡ 20% faster AI processing
  • 💰 30% lower AI token costs
  • ⚠️ May miss details in longer articles

Implement Caching

Cache Firecrawl search results by query:
import { kv } from '@vercel/kv';

async function searchWithCache(query: string) {
  const cacheKey = `search:${query}`;
  const cached = await kv.get(cacheKey);
  
  if (cached) {
    console.log('✅ Using cached search results');
    return cached;
  }
  
  const results = await searchWeb(query);
  await kv.set(cacheKey, results, { ex: 3600 }); // 1 hour TTL
  return results;
}
Savings: Eliminate repeated searches for popular topics.
Cache scraped article content by URL:
async function scrapeWithCache(url: string) {
  const cacheKey = `scrape:${url}`;
  const cached = await kv.get(cacheKey);
  
  if (cached) return cached;
  
  const content = await scrapeWithFirecrawl(url);
  await kv.set(cacheKey, content, { ex: 86400 }); // 24 hour TTL
  return content;
}
Savings: Significant for frequently analyzed sources.
Cache full analysis results for identical queries:
async function analyzeWithCache(input: string) {
  const cacheKey = `analysis:${input.toLowerCase().trim()}`;
  const cached = await kv.get(cacheKey);
  
  if (cached) {
    console.log('✅ Returning cached analysis');
    return cached;
  }
  
  const result = await generateArgumentBlueprint({ input });
  await kv.set(cacheKey, result, { ex: 3600 }); // 1 hour TTL
  return result;
}
Savings: Instant response for duplicate queries.
Caching requires a key-value store. Vercel KV (Redis) is recommended for production deployments.

Parallel Processing Optimization

The application already parallelizes scraping:
// From web-scraper.ts:96-102
const scrapePromises = urls.map(async (url) => {
  const content = await scrapeWithFirecrawl(url);
  return { url, content, source: 'Firecrawl' };
});

const scrapeResults = await Promise.all(scrapePromises);
Further optimization: Add timeout and fallback:
const scrapePromises = urls.map(async (url) => {
  try {
    const content = await Promise.race([
      scrapeWithFirecrawl(url),
      new Promise((_, reject) => 
        setTimeout(() => reject(new Error('Timeout')), 10000) // 10s timeout
      )
    ]);
    return { url, content, source: 'Firecrawl' };
  } catch (error) {
    console.warn(`Scrape timeout for ${url}`);
    return { url, content: null, source: 'Firecrawl' };
  }
});
Impact: Prevents slow sources from blocking entire analysis.

Firestore Optimization

Data Structure Efficiency

The current security model is already optimized:
/users/{userId}/argumentMaps/{mapId}
Why this is efficient:
  • ✅ User-scoped queries avoid full collection scans
  • ✅ No joins or lookups required
  • ✅ Security rules use path-based auth (no database reads)

Indexing Strategy

1

Create Composite Index for Queries

If you query argument maps by date:Index fields:
  • userId (Ascending)
  • createdAt (Descending)
Firebase Console: Firestore → Indexes → Add
2

Use Subcollection Queries

Always query within user scope:
// ✅ Efficient - user-scoped
const mapsRef = collection(db, `users/${userId}/argumentMaps`);
const q = query(mapsRef, orderBy('createdAt', 'desc'), limit(10));

// ❌ Inefficient - full collection scan
const mapsRef = collectionGroup(db, 'argumentMaps');
3

Implement Pagination

For large result sets:
const firstBatch = await getDocs(
  query(mapsRef, orderBy('createdAt', 'desc'), limit(20))
);

const lastVisible = firstBatch.docs[firstBatch.docs.length - 1];

const nextBatch = await getDocs(
  query(mapsRef, orderBy('createdAt', 'desc'), startAfter(lastVisible), limit(20))
);

Read Optimization

Expensive:
// Continuous listener - charges on every update
onSnapshot(docRef, (snapshot) => {
  console.log(snapshot.data());
});
Optimized:
// One-time read
const snapshot = await getDoc(docRef);
console.log(snapshot.data());
Cost: Listeners count as 1 read on attach + 1 read per update.
// Instead of individual reads
const doc1 = await getDoc(doc(db, 'users/uid1/argumentMaps/map1'));
const doc2 = await getDoc(doc(db, 'users/uid1/argumentMaps/map2'));

// Use a query
const q = query(
  collection(db, `users/${userId}/argumentMaps`),
  where('__name__', 'in', ['map1', 'map2'])
);
const snapshot = await getDocs(q);
Savings: 2 reads vs 2 reads, but better for larger batches.
Enable Firestore persistence:
import { enableIndexedDbPersistence } from 'firebase/firestore';

enableIndexedDbPersistence(db).catch((err) => {
  if (err.code === 'failed-precondition') {
    console.warn('Persistence failed: multiple tabs open');
  } else if (err.code === 'unimplemented') {
    console.warn('Persistence not available in this browser');
  }
});
Impact: Reduces reads for frequently accessed documents.

API Cost Optimization

Firecrawl Cost Management

Pricing (varies by plan):
  • Search: ~$0.001 per result
  • Scrape: ~$0.01 per page
Current usage per analysis:
  • 20 search results: $0.02
  • 8 scrapes: $0.08
  • Total: ~$0.10
Optimization strategies:
1

Reduce Search Results

// From web-search.ts:79
body: JSON.stringify({
  query: searchQuery,
  limit: 15,  // Reduced from 20
})
2

Filter Low-Quality Sources

Skip sources before scraping:
const urlsToScrape = searchResults
  .filter(r => r.snippet.length > 100)  // Skip low-content results
  .slice(0, 5)  // Further limit
  .map(r => r.link);
3

Implement Usage Quotas

Limit analyses per user:
const userAnalysisCount = await getUserDailyAnalysisCount(userId);
if (userAnalysisCount >= 10) {
  throw new Error('Daily analysis limit reached');
}

Google Gemini Cost Management

Pricing (Gemini 1.5 Pro):
  • Input: $3.50 per 1M tokens
  • Output: $10.50 per 1M tokens
Current usage per analysis:
  • Input: ~25,000 tokens = $0.09
  • Output: ~2,000 tokens = $0.02
  • Total: ~$0.11
Optimization strategies:
Trim scraped content more aggressively:
// From 12,000 to 6,000 chars per source
${doc.content.substring(0, 6000)}
Savings: 50% reduction in input tokens.
For search query generation (simpler task):
const searchQueryPrompt = ai.definePrompt({
  name: 'searchQueryPrompt',
  model: 'gemini-1.5-flash',  // 10x cheaper than Pro
  // ...
});
Savings: 0.001vs0.001 vs 0.01 per call.
Shorter, more direct prompts:
// Instead of verbose instructions
system: `You are an expert...[500 words of instructions]`

// Use concise, focused instructions
system: `Analyze arguments. Return JSON with: thesis, claims, evidence, fallacies.`

Twitter API Cost Management

Pricing:
  • Basic tier: $100/month (500K tweets)
  • Free tier: Deprecated for new apps
Optimization:
// Make Twitter search optional
const ENABLE_TWITTER = process.env.ENABLE_TWITTER === 'true';

if (ENABLE_TWITTER) {
  try {
    const twitterResult = await twitterSearch({ query: searchQuery });
    // ...
  } catch (error) {
    console.error('Twitter search failed, continuing.');
  }
}
Disable Twitter for:
  • Development/testing environments
  • Cost-sensitive deployments
  • Topics unlikely to have social media discussion

Performance Monitoring

Add Timing Metrics

// In generate-argument-blueprint.ts
const startTime = Date.now();

// After search
const searchDuration = Date.now() - startTime;
console.log(`⏱️ Search completed in ${searchDuration}ms`);

// After scraping
const scrapeDuration = Date.now() - startTime - searchDuration;
console.log(`⏱️ Scraping completed in ${scrapeDuration}ms`);

// After AI analysis
const analysisDuration = Date.now() - startTime - searchDuration - scrapeDuration;
console.log(`⏱️ AI analysis completed in ${analysisDuration}ms`);

// Total
const totalDuration = Date.now() - startTime;
console.log(`⏱️ Total analysis time: ${totalDuration}ms`);

Monitor API Usage

1

Track API Calls

// Add to each API wrapper
await logAPICall({
  service: 'firecrawl',
  operation: 'search',
  userId: currentUserId,
  cost: 0.02,
  timestamp: Date.now(),
});
2

Set Up Alerts

Monitor in Firestore:
const dailyCost = await getDailyAPICost();
if (dailyCost > 50) {
  await sendAlert('Daily API cost exceeded $50');
}
3

Create Usage Dashboard

Display in admin panel:
  • API calls per day
  • Cost per analysis
  • Most expensive operations
  • User-level usage

Frontend Performance

Optimize Argument Visualization

The visualization component may struggle with large argument trees. Optimization:
// Lazy load non-visible nodes
import dynamic from 'next/dynamic';

const ArgumentGraph = dynamic(() => import('@/components/ArgumentGraph'), {
  ssr: false,  // Disable SSR for heavy client-side rendering
  loading: () => <div>Loading visualization...</div>,
});

Progressive Loading

Show results as they become available:
const [status, setStatus] = useState('Searching...');
const [progress, setProgress] = useState(0);

// In the flow, emit progress events
setStatus('Scraping articles...');
setProgress(40);

// Later
setStatus('Analyzing arguments...');
setProgress(70);

Production Recommendations

Configuration:
  • Use default settings (8 sources, full content)
  • Enable Twitter for social pulse
  • Implement basic caching (1 hour TTL)
Expected cost: $10-15/day
Optimizations:
  • Reduce to 5 sources per analysis
  • Implement aggressive caching (24 hour TTL)
  • Use Gemini Flash for search query generation
  • Optional Twitter (enable for premium users only)
Expected cost: $50-100/day
Advanced optimizations:
  • Reduce to 3 sources per analysis
  • Implement multi-tier caching (Redis + CDN)
  • Pre-analyze popular topics (cron jobs)
  • Disable Twitter or use separate quota
  • Implement user quotas and rate limiting
  • Consider self-hosted scraping for frequently accessed domains
Expected cost: $200-500/day

Next Steps

Common Issues

Troubleshoot performance problems

API Errors

Debug API integration issues

Firebase Setup

Optimize Firebase configuration

Environment Variables

Configure API keys and limits

Build docs developers (and LLMs) love