How a request flows
Request lifecycle
middleware order — auth runs innermost, so the logger captures even rejected calls
Client
Authorization: Bearer <key>
→
CORS
origin allowlist
→
RequestLogger
writes api_request_log, never blocks
→
ApiKey auth
SHA-256 hash → client lookup, or admin shortcut
→
Route handler
reads request.state.client
Tier ladder
require_tier(min) gates premium reads
0freeauthenticated shared reads
1starterstandard reads
2growth · proone tier: affinity recompute, group refresh
Access & limits
how each route is protected
| shared | Bearer key required; shared catalog/data without a per-resource tenant gate |
| client | Bearer key required; caller-owned resources enforce client_id |
| tier | require_tier("pro") minimum level |
| admin | ADMIN_API_KEY shortcut → is_admin flag |
| va-scope | require_scope("va"); key locked to /va/* |
| limits | 120/min per-IP · LLM 5/min + 30/day per end user, 20,000/day per client · VA 60/min (OTP fetch 10/min) · recs 10/min |
Conventions
| Pagination | Opaque base-64 cursor (wins over offset) on all feed/trending/search routes; responses carry has_more + next_cursor. |
| Windows | Velocity windows are 6h · 24h · 72h · 7d; search uses its own 1h/12h scale and resolves to nearest. |
| Size tiers | nano · micro · mini · small · medium · large · xl — audience buckets used across velocity/trending filters. |
| Term matching | Free text is folded to a collapse key — NFKC-normalized, lowercased, # stripped, every non-[a-z0-9] character dropped — so gym tok, #GymTok and gym-tok resolve identically. Shared by /directory/resolve (as a fallback after an exact match, reported via matched_via), /suggest, /niches/from-term, and hashtag entity_ids on /explore. |
| Ownership | Models & topic-groups are per-client. The niche registry exposes canonical, caller-owned, and legacy topics; foreign-private topic IDs fail closed as 404. |
| No fabrication | Empty result sets return empty — the API never invents rows to fill a page. |