Live: https://geo-agent-five.vercel.app Stack: Next.js 16 + Gemini + Tavily + Upstash Vector + OpenRouter Theme: shadcn/ui Indigo / Neutral / Nova / Nunito Sans
GEO Agent helps users discover Taobao arbitrage opportunities by comparing Chinese supplier products against US market alternatives. Users describe a product in chat, the system runs a full multi-step analysis pipeline, and opens a detailed report in a new tab with:
- Arbitrage Comparison - Taobao vs US alternatives side-by-side with savings %
- Share of Model (SoM) - How visible you are in AI-generated answers
- Competitor Analysis - Who dominates the AI landscape for your product
- Gap Analysis - Where opportunities exist
- Generated Content - SEO/GEO-optimized blog posts, FAQs, schema markup (JSON-LD)
- Interactive Charts - Savings bar chart, SoM pie chart, competitor mentions bar chart
When multiple users search for similar products (conversation-level similarity > 0.80), the system auto-generates a static SEO blog guide at /guides/[slug] via ISR.
User Chat Input
|
v
/api/analyze (SSE streaming)
|
+--> Step 0: Store conversation in Upstash + check similarity gate
+--> Step 1: Generate search queries (Gemini)
+--> Step 2: Translate queries to Chinese (Gemini)
+--> Step 3: Search Taobao (Tavily, include_domains) + translate results to English
+--> Step 4: Search US market (Tavily + Perplexity + GPT-4o-mini in parallel)
+--> Step 4.5: Generate arbitrage comparison (Gemini)
+--> Step 5: Compute SoM scores
+--> Step 6: Embed & match products via Upstash Vector
+--> Step 7: Gap analysis (Gemini)
+--> Step 8: Content generation (Gemini)
|
v
Report opens in /report (reads from localStorage)
|
v
If similarity gate triggered --> POST /api/guides --> static guide at /guides/[slug]
src/
app/
page.tsx # Main chat UI, SSE consumer, report trigger
report/page.tsx # Standalone report page (reads localStorage)
guides/[slug]/page.tsx # ISR static guide pages (revalidate: 60s)
api/
analyze/route.ts # Main analysis pipeline (SSE, maxDuration=300)
chat/route.ts # Follow-up chat with report context
guides/route.ts # POST: generate guide, GET: list guides
guides/[slug]/route.ts # GET single guide by slug
layout.tsx # Root layout (Nunito Sans font, dark mode)
globals.css # Indigo theme (oklch color space)
components/
arbitrage-comparison.tsx # Taobao vs US side-by-side cards
chat-panel.tsx # Chat message display
competitor-table.tsx # Sortable competitor leaderboard
evidence-panel.tsx # Scrollable evidence citations (max-h-500px)
gap-analysis.tsx # Gap cards with severity badges
generated-content.tsx # Tabbed content viewer + JSON-LD rendering
progress-steps.tsx # SSE progress bar
report-dashboard.tsx # Full report layout orchestrator
som-score.tsx # SoM circular gauge + per-query breakdown
charts/
savings-chart.tsx # Horizontal bar: Taobao savings by category
som-pie-chart.tsx # Donut: Share of Model distribution
competitor-bar-chart.tsx # Grouped bar: mentions + SoM% per competitor
ui/ # shadcn/ui primitives (chart, card, badge, etc.)
lib/
gemini.ts # Gemini API: translate, queries, analyze, content, arbitrage, embedding
tavily.ts # Tavily search: Taobao (Chinese) + American market
openrouter.ts # OpenRouter: Perplexity sonar + GPT-4o-mini
upstash.ts # Upstash Vector: upsert, query, storeConversation, checkSimilarityGate
guide-store.ts # Guide CRUD via Upstash Vector metadata
chat-agent.ts # Follow-up chat agent with tool calling
types.ts # All TypeScript interfaces
utils.ts # cn() utility
types/
agent.ts # Re-exports from lib/types
interface AgentReport {
product: string;
arbitrageComparison: {
summary: string;
verdict: string;
items: ArbitrageItem[]; // Taobao vs US per need
};
somScore: {
overall: number; // 0-100%
perQuery: { query: string; mentioned: boolean; position: number }[];
};
competitors: {
name: string; mentions: number; somPercent: number;
avgPosition: number; sentiment: string; strengths: string[];
}[];
gapAnalysis: {
area: string; competitorStatus: string; sellerStatus: string;
recommendation: string; geoImpact?: string; seoImpact?: string;
}[];
generatedContent: {
type: string; title: string; content: string;
geoImpact: string; seoImpact: string; smoImpact: string;
schemaMarkup?: Record<string, unknown>; // JSON-LD
}[];
evidence: { query: string; source: string; response: string; urls: string[] }[];
taobaoSources?: { title: string; url: string; description?: string }[];
}
interface ArbitrageItem {
need: string;
taobao: { title: string; url: string; priceRange?: string; moq?: string; leadTime?: string; customization?: string };
usAlternative: { title: string; url: string; priceRange?: string; source?: string; limitations?: string };
arbitrageEdge: string;
savingsPercent?: number;
}| Variable | Description |
|---|---|
GEMINI_API_KEY |
Google Gemini API key (used for all LLM tasks) |
TAVILY_API_KEY |
Tavily search API key (Taobao + US market search) |
OPENROUTER_API_KEY |
OpenRouter key (Perplexity sonar + GPT-4o-mini) |
UPSTASH_VECTOR_REST_URL |
Upstash Vector REST endpoint |
UPSTASH_VECTOR_REST_TOKEN |
Upstash Vector REST token |
- Dimensions: 768
- Metric: Cosine
- Type: Dense
- Embedding model: Custom (Gemini text-embedding-004)
User types a product description (e.g., "brandable toy water guns for promotional distribution"). The first message triggers the full /api/analyze SSE pipeline. Progress updates stream back in real-time. When complete, the report auto-opens in a new tab.
The pipeline searches Taobao (via Tavily with Chinese queries) and the US market (via Tavily + Perplexity + GPT) in parallel. Gemini then generates a structured comparison: same need, Taobao offering vs US alternative, with pricing, MOQ, lead times, customization, and estimated savings %.
Every user's full conversation history is embedded as a single vector in Upstash. When a new analysis runs, the system checks if 1+ other users have similar conversation histories (cosine similarity > 0.80). If triggered:
- A "How to Source [Product] from Taobao" blog post is auto-generated
- Saved to Upstash as a guide document
- Served as a static ISR page at
/guides/[slug]
For the 2-person demo, minUsers=1 (1 other user triggers the gate).
After the report generates, users can ask follow-up questions in the same chat. These go through /api/chat with the full report as context, powered by Gemini function calling with 6 tools (search, translate, analyze, etc.).
Three Recharts-based visualizations using shadcn/ui <ChartContainer>:
- Savings by Category (horizontal bar) - % savings per arbitrage item
- SoM Distribution (donut/pie) - competitor share of AI-generated answers
- Competitor Mentions & SoM (grouped bar) - mentions + SoM% side by side
vercel --prod- Free tier: 60s function timeout (pipeline may need optimization)
- Pro tier:
maxDuration = 300works as configured - Set all 5 environment variables in Vercel dashboard
npm install
# Create .env.local with all 5 variables
npm run dev| Model | Provider | Purpose |
|---|---|---|
gemini-3-flash-preview |
Google Gemini | Translation, queries, analysis, content, arbitrage comparison, embeddings |
text-embedding-004 |
Google Gemini | Vector embeddings (768 dimensions) |
perplexity/sonar |
OpenRouter | AI search with citations |
openai/gpt-4o-mini |
OpenRouter | Market analysis, competitor research |
- Color: Indigo primary (
oklch(0.637 0.237 275.508)dark /oklch(0.457 0.24 277.023)light) - Base: Neutral
- Style: Nova (shadcn)
- Font: Nunito Sans (400-800 weights)
- Dark mode background:
oklch(0.205 0 0)(lightened gray, not pure black) - All colors use oklch color space
- Conversation-level embedding (not per-query) - Captures user intent across entire sessions for better similarity matching
- Taobao URLs stay in Chinese - Source links preserved as-is, only titles/descriptions translated to English
- localStorage bridge for report - Chat page stores report, report page reads it. Simple, no server state needed
- SSE streaming - Real-time progress updates during the 30-60s pipeline
- Parallel API calls - Tavily + Perplexity + GPT run simultaneously in Step 4
- Null safety guards - All Gemini JSON responses guarded against missing fields (
impactTags || [],gaps?.length,Array.isArray())