Digest
Ask anything about your vendors
Semantic search across every tracked announcement — get a direct answer, market context, and impact ratings in seconds.
Try asking about
Searching and generating analyst brief…
No results found — try a broader query or run backfill from Admin if items are missing
Direct Answer
Market Context
Vendor Analysis
Strategic Takeaway
Vendors
These sources have been dark for 7+ days. Each suggestion below was verified working (item/link count shown). Apply swaps the source URL; Dismiss keeps things as they are.
No vendors found
Markets
Define market scope to improve relevancy scoring and search quality.
User Management
Select at least one market — users must have access to at least one market.
✓ Invite sent to
No markets assigned — user sees nothing.
APNs is not configured — push notifications are unavailable.
No devices registered yet — open the iOS app on a physical device first.
No users have registered an iOS device yet.
Vendor Discovery
Auto-detected candidate vendors awaiting your review. Approving one adds it to your vendor list; nothing is added automatically.
Discovery is running in the background. New candidates will appear here in a few minutes — use Refresh to check.
No candidates.
Run discovery to scan your markets for new vendors.
System Status
Indicators show configuration by default; Run health check live-pings each service and overlays real status + latency. OK reachable · Down unreachable / bad key · Off optional, not configured. The Tavily ping uses ~1 credit. Source-level scrape health is in the Vendors tab.
Third-party vendors this platform depends on — for status checks and incident triage.
| Vendor | Role | Tier | Website |
|---|---|---|---|
| status ↗ |
Admin
Scrapes use httpx first, then the render API (if configured), then Playwright. Set SCRAPE_RENDER_API_KEY to enable the render fallback. Check per-source reasons in the source-health panel: render_http_401/402 = bad key / out of credits, http_404 = source URL moved.
Renders the URL through the actual fallback path and compares it to the static fetch. Uses ~1 render credit.
APNS_KEY_ID, APNS_TEAM_ID, APNS_BUNDLE_ID, and APNS_KEY_CONTENT in Railway to enable.
-
Score ·
Platform Documentation
Architecture, dependencies, operations guide, and cost reference for support engineers.
Analyst Intelligence is a private market intelligence platform that automatically monitors vendor announcements across configurable markets, scores them for relevance, and delivers a daily AI-generated digest by email and iOS push notification. It is designed to be operated by a single administrator with a small number of invited iOS users.
The platform runs a nightly three-stage pipeline: ingestion (fetch RSS feeds and scrape vendor websites), processing (deduplicate, prefilter, and score each item with Claude AI), and digest (compile, summarize, and deliver). Users access content via a web dashboard (API key auth) or the native iOS app (email/password JWT auth).
┌──────────────────────────────────────────────────────────────────┐
│ RAILWAY (Cloud Host) │
│ │
│ ┌──────────────────────────┐ ┌────────────────────────────┐ │
│ │ FastAPI + Uvicorn │ │ PostgreSQL (Railway DB) │ │
│ │ (Python 3.12) │◄──► 14 tables, ORM via │ │
│ │ │ │ SQLAlchemy 2.0 │ │
│ │ ┌────────────────────┐ │ └────────────────────────────┘ │
│ │ │ APScheduler │ │ │
│ │ │ 02:00 Ingestion │ │ ┌────────────────────────────┐ │
│ │ │ 04:00 Processing │ │ │ Static Files │ │
│ │ │ HH:MM Digest │ │ │ dashboard.html │ │
│ │ └────────────────────┘ │ │ about / support / privacy│ │
│ └──────────────────────────┘ └────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Anthropic │ │ Resend │ │ Apple APNs │
│ Claude API │ │ Email API │ │ Push Service │
│ (Haiku) │ │ │ │ │
└─────────────┘ └──────────────┘ └──────────────────┘
▲ ▲
│ │
┌──────────────────┐ ┌────────────────────┐
│ Web Dashboard │ │ iOS App │
│ Alpine.js + │ │ SwiftUI │
│ Tailwind CSS │ │ JWT + Keychain │
│ (API Key auth) │ │ (email/pw auth) │
└──────────────────┘ └────────────────────┘
Framework & Server
FastAPI 0.115.5 served by Uvicorn 0.32.1 (ASGI, HTTP/2). Entry point: backend/app/main.py. On startup the app initialises the database schema, starts APScheduler, and registers all API routers.
Database
PostgreSQL via SQLAlchemy 2.0 ORM (psycopg2-binary driver). Schema management uses idempotent SQL fixups that run on every startup — no Alembic migrations. Connection pool: size 5, max overflow 10.
Authentication
Web dashboard: static X-API-Key header matched against the API_KEY environment variable using hmac.compare_digest() (timing-safe).
iOS app: JWT Bearer token (HS256, 30-day expiry) issued on email + password login. Password hashed with bcrypt. One-time invite and reset codes stored as SHA-256 hashes in auth_tokens. Signature and expiry are explicitly enforced on every decode. All auth events (login, activation, password reset — success and failure) are logged with the source IP address. The JWT_SECRET is validated at startup; the server refuses to start if it is still the placeholder default.
Security Hardening
CORS: Locked to https://www.analystintelligence.ai with an explicit allowlist of methods (GET POST PATCH DELETE) and headers. The iOS app communicates via Authorization: Bearer directly and is unaffected by browser CORS.
HSTS: Every response includes Strict-Transport-Security: max-age=31536000; includeSubDomains. After one visit, browsers permanently refuse to connect over plain HTTP.
Error messages: Internal infrastructure details (platform names, environment variable names, exception text) are logged server-side only. HTTP error responses return generic messages.
Additional hardening (April 2026): Auth endpoints are rate-limited via slowapi (login: 5/15 min; forgot-password: 3/hr; set-password: 10/hr; reset-password: 5/hr). The web dashboard JWT is stored in an httpOnly SameSite=Lax cookie — no longer in localStorage — eliminating XSS token theft. Admin routes use a centralized require_admin dependency. POST /auth/logout clears the cookie server-side. JWT lifetime shortened to 7 days (was 30) to reduce the exposure window from stolen tokens. Password policy raised to 12+ characters with uppercase, lowercase, digit, and special character required. Vendor and source URLs are validated to only accept http:// or https:// schemes on create/update. User role is now read from the server via GET /auth/me on every page load — auth_role is no longer stored in localStorage, eliminating the ability to escalate the visible UI role by editing browser storage. The iOS app is unaffected — it continues to use Authorization: Bearer headers with tokens stored in Keychain. Remaining known risks: no server-side JWT revocation (a revoked token remains valid until its 7-day expiry); no TOTP/2FA.
Scheduler (APScheduler 3.10.4)
misfire_grace_time — if the service restarts within 30 minutes of the scheduled time the job fires immediately on startup. Email recipients are derived from user accounts — admin users receive every market's digest; role=user accounts receive only the markets in their access list. Only active users who have completed account setup (password set) are included. No separate email list is maintained.
AI Usage (Claude Haiku for processing & summaries · Claude Sonnet for search)
Each passing item is sent to Claude with its title, first 600 characters of content, vendor name, and market context. Claude returns a JSON payload with: relevance_score (0–100), category, headline (≤15 words), summary (2–3 sentences), why_it_matters (1 sentence), and confidence. Calibrated with few-shot examples drawn from user thumbs-up/down feedback. Max 512 output tokens.
Generated once per digest at compile time, stored in the digests.exec_summary column. Claude receives the top-scored items grouped by vendor and writes a ≤250-word analyst brief with vendor names bolded and one paragraph per vendor. Included in email, web dashboard, and iOS app.
Optional. Uses Claude Sonnet (claude-sonnet-4-6) rather than Haiku — the richer synthesis across many results benefits from the stronger model. When a user submits a search query with summarize=true, the top 12 results are sent to Claude which returns a structured AnalystBrief: direct_answer, market_context, per-vendor vendor_analyses with impact ratings, and strategic_takeaway. 90-second timeout. Requires OPENAI_API_KEY for embeddings.
Python Dependencies
| Package | Version | Purpose |
|---|---|---|
Single-page application served as a static HTML file (backend/app/static/dashboard.html). No build step — all dependencies loaded from CDN.
Lightweight reactive framework. All state lives in a single app() function. No build tooling or bundler required.
Utility-first CSS. Dark mode enabled via darkMode: 'class'. The .dark class is applied to the authenticated app container and each modal individually (not <html>), keeping the login page permanently light. Toggle preference stored in localStorage. All type="date" inputs require explicit dark:bg-slate-700 dark:text-gray-100 classes — the browser's native date picker does not inherit color-scheme automatically from Tailwind.
Primary typeface. Loaded from fonts.googleapis.com. Free, no API key required.
API key entered at login; verified against GET /markets/ before granting access. Stored in memory only (no persistence after refresh).
Tabs
Native SwiftUI app (ios/AnalystIntelligence/). No third-party Swift packages — uses only Apple system frameworks (Foundation, UIKit, SwiftUI, UserNotifications, Security). Requires iOS 16+ and an Apple Developer account for distribution.
Authentication Flow
Admin registers user via the web dashboard → system emails an 8-character invite code → user opens app, enters invite code + sets password → POST /auth/set-password returns JWT → token stored in iOS Keychain → all API calls include Authorization: Bearer <token>. JWTs expire after 30 days; user must re-login.
Key Files
| File | Purpose |
|---|---|
Push Notifications (APNs)
Device tokens registered via POST /devices/register on every app launch. Digest job pushes via Apple APNs using HTTP/2 + JWT authentication (requires APNS_KEY_ID, APNS_TEAM_ID, APNS_BUNDLE_ID, APNS_KEY_CONTENT).
Set APNS_SANDBOX=true for Xcode debug builds; false for TestFlight/App Store. When switching environments, use Admin → Clear Device Tokens to purge stale registrations.
Iterates all active vendor_sources rows. RSS / release-notes sources parsed with feedparser (24-hour lookback). Blog and press scrape sources use a tiered fetch — httpx + BeautifulSoup first, then an optional hosted render API for JS-rendered SPAs, then a shared Playwright/Chromium browser — to extract article links. Each new URL is stored as a RawItem with a URL hash for deduplication. A fetch that yields zero links is recorded as a failure with a reason (zero_links, http_404/403, fetch_error) so broken sources surface in source health; counts reset on success. At processing time the pipeline also performs full-text capture: the article body is fetched (static, SSRF-guarded), stored on the item (up to 20k chars) for archive durability and search, and a 1,500-char excerpt is fed to Claude for scoring. Sources dark for 7+ days are re-probed by the daily Source Repair job (03:30), which stages verified replacement URLs in the Vendors tab for one-click Apply/Dismiss.
Unprocessed RawItems pass through three gates: (a) fuzzy dedup — rapidfuzz title similarity against recent items; (b) keyword prefilter — rejects job postings, award announcements, speaking engagements, and customer case studies; (c) LLM analysis — Claude Haiku scores relevance (0–100) and generates headline, summary, why-it-matters, and category. Items scoring at or above the Relevance Threshold are flagged in_digest=true.
Groups in_digest=true items by market and date. Items scoring ≥ Top Story Threshold appear under "Top Stories"; items between the two thresholds appear under "Also Noted". Claude generates a per-market executive summary stored in digests.exec_summary. Compilation is idempotent — re-running does not regenerate the exec summary if it already exists. Vendor priority tiers shape the digest: high-tier vendors get a +5 relevance boost at processing time and sort first; low-tier vendors never appear as Top Stories (Also Noted only). Tiers are set per vendor in the Vendors tab.
HTML digest email rendered per market and sent via Resend to configured recipients. Apple APNs push sent to all registered device tokens. Both delivery methods record a sent_at timestamp on the Digest row. The email also includes an Activity Spikes section when any vendor posts at 3×+ its normal weekly cadence, a Source Health section whenever any of the market's active sources has had no successful fetch for 7+ days, and zero-item ("quiet day") digests carry a coverage line — items collected and sources checked — confirming the pipeline ran normally. Separately, watched categories and watch keywords (Admin → Configuration) trigger an immediate push notification for any newly processed matching item right after the overnight processing run; keyword matches additionally carry a ⚑ marker in the digest email, and pending source-repair suggestions are counted in the Source Health block.
Scoring Thresholds
Both thresholds are configurable in Admin → Configuration. Default: Relevance = 45, Top Story = 70.
Vendor discovery automatically finds candidate vendors for each market so you don't have to add them by hand. It runs weekly (Monday 03:00) and on demand from the Discovery tab. Candidates are always staged for review — discovery never adds a vendor automatically.
Candidates come from a Tavily search of the market definition (set TAVILY_API_KEY) and an LLM pass that mines already-ingested items for competitor mentions. Each is deduped against existing vendors (domain + fuzzy name) and scored for market fit by Claude Haiku against the market's mandatory features. Candidates below the match sensitivity (fit threshold) are discarded.
For each candidate, the vendor's homepage navigation is inspected to find its blog and press/newsroom index URLs (mined candidates without a site first have their website resolved via search). The detected URLs show on every candidate card and in the admin email summary.
Pending candidates appear in the Discovery tab with fit score, confidence, evidence, and detected URLs. Approving creates the vendor and auto-configures the blog/press URLs as blog_scrape / press_scrape sources, so the vendor immediately joins the ingestion → digest pipeline. A per-market summary email is sent to that market's newsletter recipients when new candidates are staged (suppressible on manual runs).
Match sensitivity is editable at runtime in the Discovery tab (stored in app_config.discovery_fit_threshold, default 60) and is separate from the article-relevance threshold. Review directories (G2, Capterra) are not scraped for candidates — discovery uses the licensed search API plus already-ingested content.
Set in Railway's Variables panel (or a local .env file for development). Changes require a service redeploy to take effect, except for digest time/timezone which can be updated live via the admin settings API.
Required
| Variable | Description |
|---|---|
Optional
| Variable | Default | Description |
|---|---|---|
The platform is hosted on Railway. The repository root contains a backend/railway.toml that Railway reads automatically.
Nixpacks — auto-detects Python, installs requirements.txt, runs Playwright browser install.
uvicorn app.main:app --host 0.0.0.0 --port $PORT
GET /health — returns {"status":"ok"}. 30-second timeout. Railway restarts on failure (max 3 retries).
SQLAlchemy create_all() + manual fixups run on every startup. Safe to redeploy at any time.
iOS Build
Before building in Xcode, update the backend URL in ios/AnalystIntelligence/Config.swift. No Swift Package Manager dependencies — standard library only. Requires an active Apple Developer account ($99/year) for distribution via TestFlight or the App Store.
When switching between sandbox (Xcode) and production (TestFlight) APNs environments, clear all device tokens from Admin → Test & Notifications → Clear Device Tokens to avoid silent delivery failures.
Adding a Vendor
Go to Vendors → Add Vendor. Enter the name, website URL, and select the market. Then expand the vendor row and add at least one Feed Source. RSS and Release Notes are the most reliable source types — both fetch a standard RSS/Atom feed URL. Blog Scrape and Press Release Scrape use Playwright and require a URL pointing to the index page, not an individual post. Monitor the source health dot (green = healthy, red = one or more failures).
Running the Pipeline Manually
In Admin → Pipeline & Jobs, run steps in order: Ingestion → Processing → Digest. Each button is disabled while the job is running. Use "Run Full Pipeline" to run all three in sequence. After ingestion, check source health for any newly-failed sources before proceeding. Each job's schedule, last-run, and next-run are shown here and at GET /admin/schedule. A fifth job, the Bi-Weekly Recap (1st and 15th at 07:00), emails a per-market rollup of the last 14 days — AI themes narrative, most-active vendors, and category mix — and can be sent on demand with POST /admin/send-recap or the Send Recap Now button.
Running Vendor Discovery
In the Discovery tab, click Run discovery to scan all markets on demand (it also runs weekly, Mon 03:00). Uncheck "Email newsletter recipients" to scan without alerting subscribers. Review pending candidates and Approve (creates the vendor and auto-configures its detected blog/press sources) or Reject. Adjust Match sensitivity to surface more or fewer candidates. The panel shows the last scan and next scheduled scan. Requires TAVILY_API_KEY for the search channel; item-mining works without it.
Resending a Digest
Send Digest automatically resets before compiling — it deletes any existing digest rows for the selected date and clears in_digest flags so items are re-evaluated from scratch. Simply pick a date and click Send Digest; no manual reset step is needed.
Adding an iOS User
Go to Admin → Authorized Users → Register. Enter the user's email and role (Admin or User). If Role = User, select which markets they can access. The system emails an invite code to the address. The user opens the iOS app, taps "First time? Set up your account", enters the code and chooses a password. Invite codes expire after 7 days; use Resend Invite if needed.
Adjusting Relevance Thresholds
In Admin → Configuration: Relevance Threshold is the floor — items below this score are excluded from the digest entirely. Top Story Threshold determines the cut between "Top Stories" and "Also Noted" — must be set higher than the relevance threshold. Changes apply to new digest compilations; reset and recompile a date's digest to apply retroactively.
Market Context
Go to Markets and expand a market to edit its AI context fields: Market Definition (what the market is), Market Purpose (why users care), Mandatory Features (required product capabilities), Optional Features (nice-to-have capabilities). These fields are injected directly into the Claude prompt for every item in that market — better context = more accurate relevance scoring.
Semantic Search Setup
Search requires an OpenAI API key (OPENAI_API_KEY environment variable). After setting the key and redeploying, run Admin → Backfill Embeddings once to generate vectors for all existing processed items. New items are embedded automatically during the processing job.
Estimates based on a typical deployment monitoring ~20 vendors across 2 markets, processing ~50 items per day, with 2–3 iOS users. Prices as of mid-2026 — verify current pricing with each provider.
| Service | Purpose | Pricing Model | Est. Monthly | Paid? |
|---|---|---|---|---|
| Estimated Total | ~$26 – $44 / mo | |||
Changelog
A plain-English record of what has changed in each release, newest first.
Mobile web navigation fixes. On phones the navigation menu is now a slide-out drawer opened from a menu button in the top header, giving access to every section — including admin-only pages like Markets, Discovery, Status, and Documentation that the old 5-item bottom bar left unreachable. The menu now scrolls, so all items are usable in landscape on short screens where the list previously ran off the bottom with no way to reach the lower entries. Tapping a destination or the dimmed backdrop closes the drawer. The tablet and desktop sidebar are unchanged.
Face ID sign-in and smoother session expiry. When an authenticated request returns 401 (expired JWT), the app now signs out softly — keeping the account email — and returns the user to the sign-in screen with a "session expired" notice instead of surfacing raw errors in whatever tab was open. A new Sign in with Face ID toggle in Preferences (Security section) stores the password in the device keychain after a one-time verification against the server plus a biometric confirmation; from then on, expired sessions auto-offer Face ID for a one-glance re-login, and the login screen shows a Face ID button. Touch ID is used on devices without Face ID. Manual sign-out still wipes all stored credentials, and a stale stored password (changed elsewhere) disables the toggle automatically. Adds NSFaceIDUsageDescription to the app's Info.plist.
Fixed the vendor-candidate discovery email. The "Review candidates" button now links to the web dashboard on the public site (new SITE_BASE_URL setting, default analystintelligence.ai) instead of the raw Railway service URL — which also used a broken /dashboard.html path. The notice is now sent only to admin users: candidate review is an admin-only function, so non-admin recipients previously landed on an access error. The email footer explains why the recipient is receiving it.
Preferences tab. A new Preferences tab in the iOS app (under More) gathers app settings in one place. Open on launch selects the starting screen: Feed (default), Morning Triage, Digest, Search, or Notifications — choosing Morning Triage opens the review deck automatically on every cold start, so the ritual becomes: open app, swipe through the deck, done. Appearance sets System / Light / Dark mode. Notification settings (per-market mutes and quiet hours) are reachable from Preferences as well as the Notifications tab. Push deep-links still take priority over the launch preference.
Four iOS additions. Home-screen widget — today's item count (small) plus the top story (medium), fed by an App Group snapshot the app writes when the digest loads; requires a one-time widget-extension target setup in Xcode (see ios/WIDGET_SETUP.md). Morning Triage — a card-deck review of the last day's relevant items: swipe right to save, left to dismiss, tap to read, with undo; reviewed items stay out of the deck for the day. Audio briefing — the Digest tab reads the day's digest aloud with on-device speech (exec summary, top stories, also-noted), with pause/stop controls. Watch-from-reader — admins can add watch keywords from any item's ⋯ menu and change vendor priority tiers by long-pressing in the Vendors list. Help topics updated to cover all four.
Help & What's New in the iOS app. A new Help tab documents every feature — feed, reader view, saved items, digests, recaps, search, notifications, watchlists, vendors, and relevance feedback. A What's New screen summarizes each release and appears automatically the first time the app opens after an update; it's also always available at the top of the Help tab. Release notes are served from a new machine-readable endpoint (GET /changelog), so backend-only releases reach app users without an App Store update. Maintenance note: new releases must add an entry to both this Changelog tab and the structured changelog list in the backend.
Three additions. Recap archive — bi-weekly recaps are now persisted server-side and browsable in the iOS app (calendar icon in the Digest tab): themes narrative, most-active vendors, category mix, and highlights for every past period. Feed swipe actions — swipe a feed card right to save/unsave it, left for relevant / not-relevant feedback, matching the thumbs buttons. Notification preferences — a settings screen in the Notifications tab (gear icon) lets each user mute pushes per market and set quiet hours in their local timezone; the push senders honour both. Digest emails are unaffected by mutes and quiet hours. New endpoints: GET /recaps, GET/PUT /me/notification-prefs.
iOS reading bundle. In-app reader view — tapping a feed or saved item now opens a full reader screen showing the captured article text (from v0.59 full-text capture) without leaving the app; items without captured text fall back to a "Read original" link. Push deep-linking — tapping a watched-keyword or watched-category push opens the reader for that exact item instead of just launching the app. Watched-keyword badges — items matching your keyword watch list now show an orange ⚑ badge on feed cards and in the reader. Backend adds one endpoint: GET /feed/{item_id}/content.
Three additions. Keyword watchlists — add watch terms in Admin → Configuration (product names, technologies, companies); matching items trigger an immediate push and carry a ⚑ marker in the digest email, matched case-insensitively against title, preview, captured article text, and AI summary. Full-text capture — the processing pipeline now fetches and stores each announcement's article body (up to 20k chars), protecting the archive against vendor-site link rot, and feeds a 1,500-char excerpt to Claude for noticeably better scoring and summaries. Dark-source auto-repair — a new daily 03:30 job re-probes the website of any vendor whose source has been dark for 7+ days and stages verified replacement URLs; suggestions appear at the top of the Vendors tab with one-click Apply / Dismiss, and the daily digest notes when repairs await review. Nothing is changed without your approval.
Four additions. Bi-weekly market recap — a new scheduled job (1st and 15th at 07:00) emails a per-market recap of the last 14 days: an AI themes narrative, most-active vendors, category mix, and highlights; also sendable on demand from Admin → Pipeline & Jobs. Watched-category push alerts — pick categories to watch in Admin → Configuration (e.g. Acquisition / M&A) and any newly processed item in one of them triggers an immediate push after the overnight run, capped at 10 per run. Activity Spikes — the daily digest flags vendors posting at 3× (or more) their normal weekly cadence, often the first sign of a launch cycle. Vendor announcement history — the iOS vendor detail sheet gained an Announcement History view: the full reverse-chronological record of everything collected for that vendor.
Five additions in one release. Vendor priority tiers — mark any vendor High / Normal / Low in the vendor form; High vendors get a +5 relevance boost and sort first in the digest, Low vendors only ever appear under Also Noted. Bookmarks — iOS users can save items (bookmark icon on feed cards) to a server-side Saved list that survives the 7-day cache and app reinstalls. Digest Source Health alerts — the daily email now flags any source with no successful fetch for 7+ days, so a silently broken scraper is visible without opening the dashboard. Quiet-day coverage stats — zero-item digests report how many items were collected and sources checked, distinguishing "nothing announced" from "pipeline broken". Full archive in iOS — the Feed tab now scrolls back through the entire item archive with automatic paging instead of stopping at the first 50 items.
Strengthened email feedback links: tokens now carry an issued-at timestamp and expire after 90 days (the "Link expired" page is now accurate), use the full SHA-256 signature instead of a truncated 64-bit one, and are signed with a key domain-separated from the API key (set FEEDBACK_HMAC_SECRET to fully decouple). Also upgraded the untrusted-content parsing stack — lxml 5.3.0 → 6.1.1 and Playwright 1.49.0 → 1.61.0 — which parse and render arbitrary vendor web pages.
Hardened server-side URL fetching. TLS certificate verification is now enabled on all outbound scrape/discovery requests (previously disabled), and every fetch of a user- or vendor-supplied URL — Add-Vendor source suggestion, homepage detection, and the Admin Test render tool — now runs through an SSRF guard that rejects private, loopback, and cloud-metadata (169.254.169.254) addresses and re-checks every redirect hop. Also made JWT_SECRET a required environment variable so the app fails closed if it is ever unset, rather than falling back to a known placeholder.
Closed a stored XSS hole in the digest Executive Summary. That summary is generated by the LLM from scraped vendor content, and it was previously rendered as raw HTML — so a malicious vendor page could have injected script that ran in the admin's authenticated session. The summary is now HTML-escaped before formatting, with only intended **bold** and line breaks re-applied.
An RSS source pointed at a plain HTML page (a page with no feed) now fails cleanly with "URL is not an RSS/Atom feed?" instead of throwing an unhandled error. Reminder: the render-API fallback only applies to Blog Scrape / Press Release Scrape sources — RSS / Release Notes sources are parsed by feedparser and never use rendering, so an HTML blog page must be added as a Blog Scrape source, not RSS.
Added a Test render tool to the Web Scraping card (Admin) that validates the render-API fallback end to end. Enter a URL and it renders it through the actual fallback path, reporting whether rendering is configured, the render status, bytes returned, and a static-vs-rendered link comparison with sample titles — so you can confirm rendering genuinely works, not just that the key is present. (The health check only validates the key.) Uses ~1 render credit per test.
Fixed the health check reporting Anthropic and Resend as Down when they're actually up. Anthropic now uses a direct GET /v1/models call (no SDK dependency). For Resend, a valid sending-only API key returns a "restricted" 401 on the domains endpoint — that means the key is good and the service is reachable, so it's now treated as OK rather than Down.
Reorganized the navigation. System Status (the status panel, live health check, and Technology Dependencies) is now its own Status tab instead of being buried in Admin. The sidebar gained a grouped System section linking Status, Documentation, and Changelog together. The Admin tab now focuses on pipeline runs and configuration.
Added a Run health check button to the System Status panel that live-pings every service — PostgreSQL (SELECT 1), Anthropic, Resend, Tavily, the render API (ScrapingBee usage), and OpenAI — and overlays the real OK / Down status with response latency, plus a last-checked timestamp. Until you run it, the indicators show configuration status. The Tavily ping uses ~1 search credit; APNs is reported from config (no cheap live ping).
Added a System Status panel at the top of Admin that monitors every core component at a glance: all four background jobs (ingestion, processing, discovery, digest) with schedule and last-run, plus every service integration (PostgreSQL, Anthropic, Resend, Tavily, render API, OpenAI, APNs) with an OK / Off / Down indicator. Added a Technology Dependencies reference card listing each third-party vendor with its role, tier, and website/status-page links. /admin/settings now reports a configured flag per integration, and ARCHITECTURE.md was refreshed (three-tier scraper extraction, source suggestions, full component monitoring).
Adding a vendor with a website now auto-suggests its blog and press sources. After you save the vendor, it interrogates the site, detects the blog and press/newsroom pages, and shows them with a count — preferring a clean RSS feed (with its item count) and falling back to a scrape (with its link count). Check the ones you want and they're added as sources; a note confirms ingestion will run at the next scheduled run. No more hunting for feed URLs by hand.
Added a heading-anchored extraction fallback to the scraper for blog/press pages where the title lives in a heading and the link is a separate element (e.g. a "Read more" button), or where the article URLs sit at a path unrelated to the listing page (e.g. a press page at /about-us/news-press linking to articles at /news/<slug>). It pairs each heading with its nearest same-site article link and excludes external, nav, header, and footer links — recovering vendor press pages that previously returned zero links.
Fixed a foreign-key error when deleting a vendor. Vendor deletion didn't clean up two newer related tables, so the delete failed for: vendors promoted from discovery (the candidate_vendors.promoted_vendor_id link) and vendors whose items had thumbs-up/down feedback (item_feedback). Deletion now clears item feedback before removing processed items, and unlinks the discovery candidate (kept as an audit record). Verified with foreign keys enforced.
The scraper no longer uses a URL/path as an item title. Some JS-rendered pages ship anchors whose visible text is just the slug placeholder (e.g. /blog/the-importance-of-…) before client-side hydration; these were being captured as garbage-titled items. They're now rejected, which also makes such JS-only pages correctly report zero_links rather than emitting junk. Real titles on server-rendered pages are unaffected.
Fixed a 400 error when adding a Release Notes source. The source-creation endpoint still validated against the old type list (rss | blog_scrape | press_scrape) and rejected release_notes — even though the dropdown, ingestion, and source health all support it. Release Notes sources can now be added (e.g. a vendor changelog RSS feed like getprimo.com/changelog/rss.xml). Also added the Release Notes label to the vendor CSV export.
Extended the scrape link-extraction fallback to handle a singular/plural mismatch between the listing URL and the article URLs — e.g. a blog index at /blogs whose posts live at /blog/<post> (rezolve.ai). The fallback now also matches a singular/plural sibling of the listing's section, while still excluding unrelated sections, nav, category, and footer links.
Fixed blog/press scrapes that returned zero links on modern site builders (Webflow, Framer, headless CMS). The scraper only recognized semantic markup (<article>, .post, h2 a…), so card-based layouts (e.g. getprimo.com/blog) matched nothing and were recorded as failures. Added a URL-structure fallback: when no semantic selector matches, the scraper collects same-site links whose path sits under the listing page (/blog/<post>), excluding nav/header/footer so menu and category links don't leak in. This also runs on the JS-rendered (Playwright) path. Note: pages returning HTTP 403 (bot-blocked) or 404 (moved URL) are a separate issue — 403s need the hosted render API enabled; 404s need the source URL updated. Admin → now shows a Web Scraping status card indicating whether the render API is configured (and which provider), so you can confirm the SCRAPE_RENDER_API_KEY fallback is actually live.
Vendor discovery now appears in status reporting: the discovery job shows its schedule, last scan, and next scan in /admin/schedule and on the Discovery panel, with last-run persisted across restarts. Documentation was brought up to date — the in-app Docs tab gained a Vendor Discovery section plus updated pipeline, environment-variable, operations, and cost references, and ARCHITECTURE.md now reflects the live system (discovery subsystem, status reporting, current scraper, and costs).
Candidates discovered by mining existing items (which arrive with just a company name) now get their website resolved automatically via search before the blog/press scan — so they too get their feed URLs detected and auto-configured on approval, just like search-discovered vendors. Once a site is resolved, the candidate is re-checked against existing vendors by domain to avoid adding a duplicate under a different name.
Discovery now detects each vendor's blog and press/newsroom URLs from the homepage navigation. The detected URLs show on every candidate card and in the admin email summary. When you approve a candidate, those URLs are automatically configured as blog and press scrape sources, so the new vendor immediately joins the feed and newsletter pipeline — no manual source setup needed. URLs that can't be detected are flagged "not detected" so you can add them by hand.
Added a Match sensitivity control to the Discovery panel — the minimum market-fit score (0–100) a candidate must reach to be staged for review. This is now a separate, runtime-editable threshold from the article-relevance threshold, so you can loosen vendor matching (lower the number to surface more candidates) without affecting digest scoring. Saved instantly and applied to the next scan; defaults to 60.
The Discovery tab now has an "Email newsletter recipients about new candidates" checkbox next to Run discovery. Leave it on for the normal behavior; uncheck it to run a scan without alerting subscribers (useful for ad-hoc or test runs). The scheduled weekly scan always sends notifications.
Added a Discovery admin tab with a review queue for auto-detected candidate vendors. Run a scan on demand, filter by pending/approved/rejected, and see each candidate's market-fit score, confidence, evidence, and source. Approve adds the vendor to your list; Reject dismisses it — nothing is added automatically, and a badge shows the pending count. Discovery also now emails a market's newsletter recipients a single summary whenever a scan surfaces new candidates, clearly flagged as pending review.
Added automatic vendor discovery (scaffold). A weekly job — and an on-demand admin endpoint (POST /admin/discovery/run) — finds candidate vendors for each market from two channels: a Tavily search of the market definition (set TAVILY_API_KEY to enable) and an LLM pass that mines already-ingested items for competitor mentions. Candidates are deduped against existing vendors, scored for market fit by Claude Haiku using the market's mandatory-features context, and staged in a review queue. Nothing is added to your vendor list automatically — discovery only ever proposes; approving a candidate (POST /admin/discovery/candidates/{id}/approve) is what creates the vendor. Tunable via DISCOVERY_FIT_THRESHOLD.
Added a Vendor filter to the Feed on both the web dashboard and the iOS app. Pick a single vendor to narrow the feed to just their items; it works alongside the existing market, category, and score filters. The vendor list shows every vendor you have access to.
Fixed silent scrape failures. A blog/press page that returned zero article links — because it moved (404), blocked us (403), or is JavaScript-rendered — was being recorded as a successful check, so broken sources never showed as failing and never auto-disabled. Zero-link scrapes now count as failures, increment the failure counter, and surface their reason (zero_links, http_404, http_403, fetch_error) in the source health panel. Also added an optional hosted rendering-API fallback (ScrapingBee / Firecrawl / Browserless / custom, configured via SCRAPE_RENDER_API_KEY) so JavaScript-rendered vendor pages can finally be read.
Fixed FK violation in Dedup Cleanup — digest item rows referencing processed items are now deleted first before the processed items themselves, preventing the constraint error on cleanup runs.
Added Release Notes as a new vendor source type. Works identically to RSS — point it at a vendor's changelog RSS/Atom feed and it will be ingested and scored alongside blog posts and press releases. Release Notes sources show a distinct amber badge in the source health panel and vendor list.
Full Run now truly waits for each pipeline step to finish on the backend before starting the next — previously the step sequence was fire-and-forget. The digest step in Full Run always targets today's date and auto-resets the existing digest before recompiling. Fatal errors in any background job (ingestion, processing, digest) now trigger an immediate alert email to the admin.
Send Digest now auto-resets before compiling, so there's no longer a need to click Reset Digest first. The Reset Digest button has been removed from the admin panel. The backend deletes any existing digest rows for the target date and clears in_digest flags before recompiling, making every Send Digest idempotent.
Fixed scraper process exhaustion on Railway. Blog and press scrape sources were each launching a fresh Chromium browser process, hitting the container's process limit (EAGAIN) after a handful of sources. The ingestion pipeline now launches a single shared browser once per run and routes all scrape sources through it sequentially, reducing Chromium process spawns from N (one per source) to 1.
iOS feedback submission. A feedback button (conversation icon) in the Vendors tab toolbar opens a native sheet with subject and message fields. Submitting POSTs to POST /app-feedback with source: "ios", shows a success confirmation, and appears alongside all other submissions in the web admin Feedback tab.
Vendor CSV export. A new Export CSV button in the Vendors tab opens a market selection dialog pre-populated with all markets you have access to. After confirming your selection the browser downloads a vendors.csv file containing each vendor's market, name, website, and all configured feed sources (one row per source).
User feedback system. Any authenticated user can submit platform feedback (bug reports, feature requests, questions) via a new modal accessible from the sidebar on web and from the iOS app. Submissions are stored in the database with submitter info and status tracking. Admins get a new Feedback tab with a full list of all submissions, the ability to update status (Open / In Progress / Resolved), add internal notes, and reply to users directly by email. Replying automatically marks the item Resolved. Support page updated with JWT sign-in instructions, expanded FAQs, and App Store link.
Public marketing home page at analystintelligence.ai. Elegant dark hero design, AI + human intelligence partnership framing, feature highlights, and "by invitation, free" access messaging. Updated About page with Tom Cipolla's professional biography, career timeline, expertise areas, LinkedIn and contact links. No tech-stack disclosure on public pages.
Full mobile phone responsive layout. On small screens the sidebar hides and a bottom tab bar appears with Search, Feed, Digest, Vendors, and Admin. A compact top header shows the current section. Content padding, the Admin pipeline grid, and filter bars all reflow cleanly at phone width. Tablet and desktop layouts are unchanged.
Fixed iOS (and web) showing no digest for today's date. Digests were being stored under yesterday's date instead of the generation date, so requesting today always returned 404. The digest job now files entries under the date it runs. Added a graceful API fallback so that requesting today before the digest job completes returns the most recent available digest instead of a hard 404.
Added "Legal & Privacy" section to the left nav. Includes quick links to the Privacy Policy, Support, and About pages; a Hosting & Infrastructure table; and a Data Processing Agreement stating that user data will never be sold or used outside the application.
Pipeline last-ran timestamps now persist to the database and survive server restarts and deployments. Previously, "Last ran" on the Admin page would reset to "—" after every deploy.
Each pipeline step on the Admin page now shows its scheduled run time, when it last ran, and when it will run next — all displayed in your local timezone. Last-ran times are now also tracked for jobs that run automatically on the schedule, not just those triggered manually.
Added a refresh button to the Changelog page. Since changelog entries are part of the application itself, the button reloads the page so the latest entries are reflected without manually refreshing the browser.
Added refresh buttons to the Digest, Vendors, and Markets pages so you can reload data without navigating away or doing a full page reload. Feed already had one; the button style is consistent across all tabs.
The executive summary in each newsletter is now shorter and punchier — one sentence per vendor, under 80 words total, focused on the single most important headline. On quiet days with no notable updates, the summary now says so explicitly rather than being omitted from the email entirely.
Added a Re-embed All button in Admin → Semantic Search. Use this after an embedding model upgrade to clear and regenerate all stored vectors. The existing button is now labelled Backfill New and continues to handle routine catch-up of items missing embeddings.
Deleting a vendor now permanently removes it and all associated data (sources, raw items, processed items, digest entries). Previously, deleted vendors would reappear after a page reload because the system was only marking them inactive rather than removing them.
Fixed a deployment failure on Railway that prevented the rate-limiting feature from loading, causing the application to crash on startup. The build process now guarantees all dependencies are installed even when the build cache is stale.
Migrated semantic search from OpenAI's text-embedding-3-small (deprecated October 2026) to text-embedding-3-large. Search quality improves with the upgrade. After this deployment, run Admin → Backfill Embeddings once to regenerate stored vectors — existing search results will be degraded until the backfill completes.
Vendor cards now show the correct source count immediately when you add or remove a feed source — no page reload needed. You can now also change which market a vendor belongs to directly from the Edit Vendor screen. A new Changelog tab was added to the bottom of the navigation panel so releases are easy to review.
Session tokens now expire after 7 days instead of 30, reducing the window of exposure if a token is ever stolen. Password requirements were strengthened — passwords must now be at least 12 characters and include uppercase, lowercase, a number, and a special character. Feed source and vendor website URLs are now validated to block invalid schemes. The user role shown in the sidebar is now verified directly with the server on every page load rather than being read from browser storage, preventing anyone from faking admin access in the interface.
Login and password reset endpoints now enforce automatic lockouts after repeated failed attempts, preventing brute-force attacks. After signing in on the web dashboard, your session token is now stored in a secure browser cookie that JavaScript cannot read — eliminating a class of attacks where malicious content could steal your login. Admin-only features are now protected by a single centralised check rather than scattered guards, making it easier to maintain consistently. Signing out now immediately invalidates the session on the server.
Fixed a crash that prevented the Digest view from loading. Navigating to a specific date's digest would fail with an error; it now loads correctly.
Completed a full security review of the platform. Hardened the application against common web attacks: locked down which websites can communicate with the API, enforced HTTPS-only connections, strengthened how login tokens are validated, masked internal error details from being visible in the browser, and ensured all API key checks are resistant to timing-based guessing attacks. All authentication events (successful and failed) now record the source IP address for audit purposes.
Added a User Management section for administrators. You can now see when each user last logged in, whether they have the iOS app installed, and the date they registered their device. Added the ability to send a push notification directly to a specific user's device, as well as a broadcast tool to send a custom notification to all users or a selected subset.
New users can now activate their account directly from the sign-in screen using the invite code sent to them by email. Previously this step was only accessible from the iOS app. The invite email now includes a direct link to the platform for easier onboarding.
The Top Story relevance threshold is now configurable from the Admin panel without needing a code change. Digest email recipients are now drawn from the registered user list rather than a hardcoded email address. Improved the user registration flow with a confirmation message on success. Fixed a visual issue with date inputs in dark mode. Added a documentation tab for administrators covering the full platform architecture.
Initial web dashboard and iOS backend launch. Includes feed monitoring across multiple markets and vendors, daily digest email delivery, the search feature with AI-generated analyst briefs, user authentication with invite-based onboarding, and iOS push notifications.
Initial platform build. Core pipeline established: vendor source monitoring, RSS and web scraping, AI-based relevance scoring, and digest compilation. Deployed to Railway.
User Feedback
Review, respond to, and track feedback submitted by all users.
No feedback submissions yet.
Admin Notes
Reply to Feedback
Sending email to
After sending, the feedback will automatically be marked as Resolved.
Legal & Privacy
Policies, hosting details, and your data rights.
All application data and processing takes place on the following platforms. No data leaves this infrastructure except as described in the Privacy Policy.
This Data Processing Agreement ("DPA") describes how Analyst Intelligence ("the service," "we") processes data provided by or generated on behalf of authorized users ("you," "the user"). It applies to all data submitted to, stored in, or processed by the service.
We will not sell, rent, license, or otherwise transfer your data or any data generated through your use of the service to any third party for commercial purposes. Your data is not an asset that changes hands in any transaction involving the service.
Data collected through the service — including vendor intelligence content, user accounts, feedback signals, device tokens, and email addresses — is used exclusively to operate and improve the service for you. It is not used for advertising, cross-service profiling, or any purpose outside the functionality described in the application.
The service uses the sub-processors listed in the Hosting & Infrastructure section above to deliver its functionality. Data is shared with these services only to the extent necessary for the specific function they perform (e.g., AI analysis, email delivery). All sub-processors are required under their own terms not to use your data for purposes beyond the service call.
All data in transit is encrypted via TLS. Data at rest is stored in a private, non-publicly-accessible PostgreSQL database. Access requires authentication credentials that are never stored in plain text.
Users may request deletion of their account and associated data at any time by contacting the administrator. Vendor intelligence content (items, digests) is retained to support the historical search and digest features; individual user data (account, feedback, device tokens) is deleted promptly upon request.
Questions about this agreement or data practices can be directed to [email protected].