A Vercel AI SDK tool integrated into the chatbot that analyzes X/Twitter data in real-time to determine optimal posting times for marketing content. The tool uses xAI's Grok API to query live social media signals and returns scored recommendations.
- Framework: Vercel AI SDK 5.x with tool integration
- API: xAI Grok API (https://api.x.ai/v1/chat/completions)
- Model: Grok 2 (configurable via
MARKETING_MOMENT_MODELenv var) - UI: React 18 with Framer Motion for progress animations
- State: React Context API for progress tracking
- Streaming: Vercel AI SDK's
UIMessageStreamWriterfor real-time updates
lib/ai/tools/
└── marketing-moment-analyzer.ts # Core tool (379 lines)
components/
└── marketing-progress.tsx # Progress UI (76 lines)
hooks/
└── use-marketing-progress.tsx # State management (57 lines)
app/(chat)/api/chat/route.ts # Tool registration
lib/ai/prompts.ts # System prompt
components/data-stream-handler.tsx # Stream processing
User Question
↓
Grok 3 LLM detects timing intent
↓
Calls analyzeMarketingMoment tool
↓
4 parallel Grok API queries (trends, sentiment, competition, risks)
↓
Each query returns JSON with signals + insight
↓
Signals weighted and summed (base score: 50)
↓
Score mapped to GREEN/YELLOW/RED recommendation
↓
Progress streamed to UI via data-marketingProgress events
↓
Final response with actionable guidance
POST https://api.x.ai/v1/chat/completions
Headers:
Content-Type: application/json
Authorization: Bearer ${XAI_API_KEY}
Body:
{
model: "grok-2",
temperature: 0.3,
messages: [
{ role: "system", content: "..." },
{ role: "user", content: "..." }
],
search_parameters: {
mode: "on",
sources: [{ type: "x" }],
return_citations: false,
max_search_results: 10
}
}Each of 4 queries asks Grok to search X/Twitter for specific signals:
- Trends (6-hour window): Post volume, hashtags, influencer activity
- Sentiment (3-hour window): Emotions, positive/negative ratio, shifts
- Competition (12-hour window): Competitor posts, campaigns, share of voice
- Risks (24-hour window): Controversies, regulatory news, backlash
Expected JSON from each query:
{
"insight": "Posts about coffee increased 40% in last 3 hours...",
"signals": ["TRENDING_HIGH", "INFLUENCER_SUPPORT"]
}- Starts at 50 (neutral)
| Category | Signal | Weight |
|---|---|---|
| Trends | TRENDING_SURGE | +20 |
| TRENDING_HIGH | +15 | |
| TRENDING_STEADY | +8 | |
| TRENDING_LOW | -6 | |
| TRENDING_DROP | -12 | |
| INFLUENCER_SUPPORT | +6 | |
| INFLUENCER_PUSHBACK | -8 | |
| FRESH_NEWS | +10 | |
| DATA_GAP | -2 | |
| Sentiment | SENTIMENT_POSITIVE | +15 |
| SENTIMENT_WARM | +10 | |
| SENTIMENT_NEUTRAL | +4 | |
| SENTIMENT_MIXED | +2 | |
| SENTIMENT_NEGATIVE | -12 | |
| SENTIMENT_VOLATILE | -8 | |
| DATA_GAP | -2 | |
| Competition | COMPETITOR_SILENT | +12 |
| COMPETITOR_LIGHT | +6 | |
| COMPETITOR_ACTIVE | -4 | |
| COMPETITOR_SPIKE | -8 | |
| COMPETITOR_DOMINATING | -12 | |
| DATA_GAP | -2 | |
| Risks | RISK_NONE | +5 |
| RISK_MINOR | -6 | |
| RISK_SENSITIVE | -12 | |
| RISK_CRISIS | -20 | |
| POTENTIAL_BACKLASH | -10 | |
| REGULATORY_SCRUTINY | -8 | |
| RISK_UNKNOWN | -3 | |
| DATA_GAP | -3 |
totalScore = BASE_SCORE
+ sum(trends signals)
+ sum(sentiment signals)
+ sum(competition signals)
+ sum(risks signals)
// Clamped to 0-100
finalScore = Math.max(0, Math.min(100, Math.round(totalScore)))- 70-100: GREEN - "Post now"
- 40-69: YELLOW - "Monitor before posting"
- 0-39: RED - "Hold off"
type MarketingMomentResponse = {
score: number; // 0-100
recommendation: "GREEN" | "YELLOW" | "RED";
summary: string; // One-line summary
analysis: {
trends: { score: number; insight: string };
sentiment: { score: number; insight: string };
competition: { score: number; insight: string };
risks: { score: number; insight: string };
};
action: string; // Specific action guidance
bestTimeWindow: string; // e.g., "Next 1-2 hours"
topic: string; // User's topic
analyzedAt: string; // ISO timestamp
};Location: app/(chat)/api/chat/route.ts
const activeTools = selectedChatModel === "chat-model-reasoning"
? undefined
: ["getWeather", "createDocument", "updateDocument",
"requestSuggestions", "analyzeMarketingMoment"];
tools: {
analyzeMarketingMoment: analyzeMarketingMoment({ dataStream }),
// ... other tools
}Input Schema:
{
topic: z.string().min(1), // Required
context: z.string().max(400).optional() // Optional
}Location: lib/ai/prompts.ts
The marketingMomentPrompt teaches the LLM when to use the tool:
Trigger patterns:
- "Is it a good time to..."
- "Should I launch..."
- "Is now good for posting..."
- "When should I announce..."
Not triggered for:
- General marketing advice
- Historical questions
- Opinion requests
- starting - Initial message
- trends - Analyzing trends query
- sentiment - Analyzing sentiment query
- competition - Analyzing competition query
- risks - Analyzing risks query
- complete - Final result
Tool → Stream:
dataStream.write({
type: "data-marketingProgress",
data: {
stage: "trends",
insight: "Posts increased 40%...",
score: 15
}
});Stream → UI:
// data-stream-handler.tsx processes events
if (delta.type === "data-marketingProgress") {
addStage(delta.data);
}
// marketing-progress.tsx displays animated progress
{stages.map(stage => (
<motion.div>
{stageConfig[stage.stage].icon} {stage.insight}
</motion.div>
))}Progress UI auto-clears 3 seconds after completion (configurable via COMPLETION_DELAY_MS).
| Scenario | Behavior |
|---|---|
Missing XAI_API_KEY |
Throws error: "Missing XAI_API_KEY" |
| API timeout | Returns fallback with DATA_GAP signal. Timeouts: trends (16s), sentiment (12s), competition (10s), risks (10s) |
| API rate limit (429) | Returns user message: "xAI API is currently overloaded" |
| Malformed JSON | Parses raw text as insight with DATA_GAP signal |
| Network error | Returns dimension score: 0, insight: error message |
| All queries fail | Returns YELLOW fallback response |
Each query failure is isolated - other queries continue. If all 4 fail, returns:
{
score: 45,
recommendation: "YELLOW",
summary: "Unable to query live signals. Defaulting to cautious guidance.",
// ... fallback data
}# Required
XAI_API_KEY=xai-...
# Optional
MARKETING_MOMENT_MODEL=grok-2 # Default: grok-2- Tool function exported from
lib/ai/tools/marketing-moment-analyzer.ts - Registered in
route.tsactive tools array - System prompt added to
lib/ai/prompts.ts - Type added to
lib/types.ts(MarketingMomentProgress) - Stream handler in
data-stream-handler.tsx - Progress component in
components/marketing-progress.tsx - Context provider in
hooks/use-marketing-progress.tsx - Provider wrapped in
app/(chat)/layout.tsx - Component rendered in
components/chat.tsx
1. "Is it a good time to open a coffee shop in Detroit?"
2. "Should I launch my AI startup now?"
3. "Is now good for posting about electric vehicles?"
4. "We're launching sneakers in NYC tonight—safe to announce?"
- Tool triggers automatically from natural language
- Progress UI shows 5 stages with animated transitions
- Final response includes score, recommendation, and specific action
- Response time: 5-10 seconds (4 parallel API calls)
- Queries: 4 parallel requests (not sequential)
- Timeouts: Variable per query type (trends: 16s, sentiment: 12s, competition/risks: 10s)
- Total time: ~16-18 seconds (limited by slowest query - trends)
- Fallback: Graceful degradation if queries timeout
- Requires xAI API key with X search enabled
- Only works with Grok 3 model (not reasoning model)
- No caching - fresh query each time
- No historical tracking
- Rate limited by xAI API quotas
- English language only (Grok limitation)
Edit SIGNAL_WEIGHTS object in marketing-moment-analyzer.ts
Modify the ternary in evaluateMarketingMoment:
const recommendation: Recommendation =
totalScore >= 70 ? "GREEN" : // Adjust 70
totalScore >= 40 ? "YELLOW" : // Adjust 40
"RED";- Add to
SIGNAL_WEIGHTS[dimension] - Add to valid signals list in
QUERY_BLUEPRINTS - Grok will return new signal when detected