79 · AI Deep Summary button on /recommendations cards
Date: 2026-05-19 Status: shipped (migration pending apply)
Goal
Per-card "AI Deep Summary" button on the /recommendations page (RecoCard,
RecoHeroCard) and on the DeclarationCard (used on /company/[slug] and
/insider/[slug]). Generates a ~250-word grounded analyst note for one
declaration on demand, gated by per-tier daily quota.
Inputs fed to the LLM
For one declarationId we gather, in a single pass:
- Declaration row · insider name, role, transaction nature, total amount (EUR), % of market cap, signal score, isCluster flag, filing URL, PDF URL, ISIN.
- Insider's past trades · last 5 declarations by the same insider on any company (direction, date, amount). Used to detect accumulation / pattern / one-off behavior.
- Company news · last 5
CompanyNewsrows in the last 30 days (title, publisher, link, summary). - Web snippets · top 5 results from Brave Search API
(
BRAVE_SEARCH_API_KEY) then Tavily (TAVILY_API_KEY) as fallback, query ="{company} insider {buy|sale} {year}". Returns [] if neither provider configured. Live web context, no scraping. - Company context · sector tag (locale-aware), market, marketCap.
The model never browses, never reads the filing PDF directly; everything is pre-fetched and serialized into one JSON block. Anti-hallucination is enforced by the system prompt: "summarise ONLY what is in the JSON".
Prompt template (EN)
You are a senior insider-trading analyst. Read the JSON payload provided by the user and write a single grounded analyst note about the signal. Plain text only, no markdown, no headings, no bullet points, no em-dashes, no en-dashes, no emoji. Maximum 250 words. Cover, in this order: (1) macro / news context if news or web contain something concrete, otherwise say context is limited; (2) pattern observed: cluster, isolated, accumulation, based strictly on isCluster and insider.pastTrades; (3) one non-obvious insight derived from the insider's past trades; (4) known risks if any appear in news/web snippets, otherwise say none surfaced; (5) conviction level on a low/medium/high scale with a one-sentence justification grounded in the data. End with a single sentence: "This is not investment advice." Cite news or web URLs by title in parentheses. Never invent insiders, numbers, or events.
FR mirror in src/lib/ai/topic-summary-generator.ts.
Tier limits
| Role / tier | Daily quota (UTC day) | Counter row |
|---|---|---|
role=admin |
unlimited | not incremented |
subscription=QUANT |
unlimited | not incremented |
betaUnlimited=true |
unlimited | not incremented |
subscription=PRO |
10 | UserDailyCounter kind="ai_summary" |
subscription=FREE |
2 | UserDailyCounter kind="ai_summary" |
| anonymous | 0 (401) | n/a (UI shows register CTA) |
Reset is implicit at UTC midnight (each new day = new row).
Endpoint
POST /api/ai/topic-summary body { declarationId, locale? }.
Response on success:
{
"summary": "string (<=320 words)",
"sources": [{ "title": "...", "url": "https://...", "kind": "filing|news|web" }],
"tokensIn": 1500, "tokensOut": 350,
"model": "gpt-4o-mini",
"cached": false,
"quota": { "used": 1, "limit": 2, "remaining": 1, "exceeded": false, "exempt": false, "tier": "FREE" }
}
Error codes: ANON_BLOCKED (401), QUOTA_EXCEEDED (429), LLM_ERROR (502),
Declaration not found (404).
Cache
Per-row in the new AiTopicSummary table. One row per
(declarationId, userId, locale). Reads also accept the shared row
(userId IS NULL) so admins can pre-warm. Freshness window is ttlHours
(default 168 = 7 days). Cache hits do NOT increment the quota.
Cost model
gpt-4o-mini priced at $0.15 / 1M input toks, $0.60 / 1M output toks.
Typical generation: ~1.5k input toks + ~350 output toks.
Per summary: 1.5e3 / 1e6 * 0.15 + 350 / 1e6 * 0.60 ≈ $0.000435.
At ~$0.44 per 1000 summaries, well under the $0.20 / 1k stated cap. Web search APIs (Brave free tier 2k/month, Tavily free tier 1k/month) cover realistic load without paid plans.
Architecture
+-------------------+ POST /api/ai/topic-summary
| RecoCard / | -----------------------------> +-----------------+
| RecoHeroCard / | | route.ts |
| DeclarationCard | <--------------------------- +-----------------+
+-------------------+ summary + sources + quota |
| gatherTopicInputs()
v
+---------------------+
| Declaration (DB) |
| CompanyNews (DB) |
| Insider past 5 (DB) |
| webSearch() |
| ├ Brave |
| └ Tavily fallback |
+---------------------+
|
v
+---------------------+
| OpenAI gpt-4o-mini |
+---------------------+
|
v
AiTopicSummary cache + UserDailyCounter ++
Files
prisma/migrations/20260521100000_ai_topic_summary/migration.sqlprisma/schema.prisma·AiTopicSummarymodel +UserDailyCounter.kindcommentsrc/lib/ai/topic-summary-generator.tssrc/lib/quota/ai-summary-quota.tssrc/lib/quota/user-daily-counter.ts·CounterKindwidened to includeai_summarysrc/lib/search/web-search.tssrc/app/api/ai/topic-summary/route.tssrc/components/ai/AiSummaryButton.tsxsrc/components/RecoCard.tsx· button in expanded detailsrc/components/RecoHeroCard.tsx· button under evidence bulletssrc/components/DeclarationCard.tsx· button compact, in action stack
Env vars
OPENAI_API_KEY· required (already set in prod for nightly cron)BRAVE_SEARCH_API_KEY· optional, preferred web providerTAVILY_API_KEY· optional fallback