# Best Times Implementation

**Date:** 2026-04-06  
**Status:** Updated — follows OpenAI/Anthropic social practice pattern

## Overview

`GET /api/v1/analytics/best-times?track=DEVELOPER&networks=TWITTER,LINKEDIN`

Returns the best times to post for a given content track. Returns **3 time slots** with day, hour, and source attribution.

## Design Philosophy: No Real-Time Feedback Needed

This endpoint follows the posting patterns used by **OpenAI** (@OpenAI, 4.3M+ followers) and **Anthropic** (@AnthropicAI, 1M+ followers), which rely on **proven industry research** rather than real-time engagement feedback loops.

### Why real-time feedback isn't needed:

1. **B2B posting patterns are stable** — work-hour behavior doesn't shift week-to-week
2. **X/Twitter engagement window is ~18 minutes** — too short for meaningful same-session feedback
3. **Industry consensus across 4+ major studies** provides higher-confidence recommendations than any single account's limited post history
4. **Accounts with <50 published posts** don't have enough data for statistically meaningful empirical analysis

### Research sources:

- **Sprout Social**: 2.7B engagements, 470K profiles analyzed (May–Sep 2025)
- **Buffer**: 8.7M tweets analyzed (2026)
- **Hootsuite**: B2B brand benchmarks (2025)
- **HubSpot**: Social media benchmarks (2025)
- Full analysis: `SOCIAL_OPERATIONS_RESEARCH.md` §1 "Posting Hours & Frequency"

### Consensus finding:

> **9 AM – 12 PM ET, Tuesday through Thursday** is the highest-confidence posting window for B2B tech across all major studies.

## Response Strategy

The endpoint uses a **layered priority system** that guarantees a 200 response:

### Layer 1: TrackConfig (highest priority)
If an admin has customized `bestWindows` for this track in the database, those override everything else.

### Layer 2: Empirical Analytics (secondary, optional)
When there are ≥50 published posts in the last 90 days for this track, actual performance data is used to augment recommendations. This is a **bonus signal** — the endpoint works fine without it.

### Layer 3: Research-Backed Best Practice (guaranteed fallback)
Industry-consensus posting times per track. This is the **guaranteed fallback** — the endpoint always returns valid data even with zero published posts or a down database.

### Per-Track Best Practice Windows (EST / UTC-5):

| Track | Slot 1 | Slot 2 | Slot 3 |
|---|---|---|---|
| PRODUCT | Tue 9am | Wed 10am | Thu 9am |
| EVENT | Tue 10am | Wed 9am | Thu 10am |
| DEVELOPER | Tue 10am | Thu 11am | Wed 10am |
| THOUGHT_LEADERSHIP | Wed 9am | Thu 9am | Tue 10am |
| EMPLOYEE | Mon 12pm | Wed 12pm | Fri 12pm |
| ENTERPRISE | Tue 9am | Thu 9am | Wed 10am |
| TECH_EDGE | Wed 10am | Thu 10am | Tue 11am |

**Rationale for per-track adjustments:**
- **DEVELOPER**: Developers start later → 10–11 AM instead of 9 AM
- **EMPLOYEE**: Personal social browsing peaks at lunch → 12 PM
- **EVENT**: Event announcements work best before lunch → 9–10 AM
- **THOUGHT_LEADERSHIP**: Thought pieces are read first thing → 9 AM

## Response Format

```json
{
  "slots": [
    {
      "day": "TUE",
      "dayNumber": 2,
      "hour": 10,
      "label": "Tuesday 10am",
      "source": "track_config" | "analytics" | "best_practice"
    }
  ],
  "track": "DEVELOPER",
  "dataPoints": 127,
  "methodology": "analytics_augmented" | "track_config" | "best_practice"
}
```

### Methodology values:
- `best_practice`: Using research-backed defaults (most common for new accounts)
- `track_config`: Using admin-customized windows
- `analytics_augmented`: Empirical data is available and was used (≥50 data points)

## TiFlash Dependency

**None.** This endpoint queries the Post table directly with a simple `COUNT(*)` GROUP BY aggregation. This is efficiently handled by TiKV's row-based storage. TiFlash hints are unnecessary and would add complexity without benefit.

The query pattern is:
```sql
SELECT DAYOFWEEK(publishedAt), HOUR(publishedAt), COUNT(*) AS cnt
FROM Post
WHERE status = 'PUBLISHED'
  AND publishedAt IS NOT NULL
  AND publishedAt >= ?
  AND track = ?
GROUP BY DAYOFWEEK(publishedAt), HOUR(publishedAt)
ORDER BY cnt DESC
```

This is lightweight — it only counts rows in time buckets. TiFlash is designed for heavy aggregations (SUM, AVG, multi-table JOINs) where columnar storage provides 3–10x speedup. For a simple COUNT on a single table, TiKV is sufficient.

## Error Handling

The endpoint **never returns 500**. All database queries are wrapped in try/catch:
- If the track config query fails → fall back to research-backed defaults
- If the empirical analytics query fails → fall back to config or research-backed defaults
- If there's no track parameter → return generic best practice times

## OpenAI/Anthropic Pattern Match

| Aspect | OpenAI | Anthropic | Our Implementation |
|---|---|---|---|
| Primary timing source | Industry research | Industry research | Research-backed defaults |
| Secondary signal | Launch spikes | Research cadence | Empirical analytics (≥50 posts) |
| Weekday focus | Mon–Fri business hours | Mon–Fri business hours | Tue–Thu 9 AM–12 PM ET |
| Weekend posting | Rare | Almost never | Not recommended (EMPLOYEE track is exception) |
| Posting frequency | 2–5/day | 1–3/day | Configured per track |

## Related Files

- **Route handler**: `app/api/v1/analytics/best-times/route.ts`
- **Research source**: `SOCIAL_OPERATIONS_RESEARCH.md` §1
- **TiFlash docs**: `docs/TIFLASH_SETUP.md` (best-times excluded — no TiFlash needed)
- **Frontend component**: `components/composer/TimeSlotPicker.tsx` (reads trackConfig directly)
- **Time slots utility**: `lib/time-slots.ts`
- **E2E test**: `playwright/tests/module-g-error-handling.spec.ts` (ERR-14, PERF-03)
