Pulled from ClinicalTrials.gov v2 API. Filtered to studies whose intervention type is DEVICE, DIAGNOSTIC_TEST, or COMBINATION_PRODUCT. Watermarked on lastUpdatePostDate with a 14-day lookback, so updates to a trial's status (e.g. RECRUITING → COMPLETED) are picked up on the next refresh, not just new trials.
| Column | Notes |
|---|---|
| nct_id | Primary key. Always NCT + 8 digits. |
| brief_title / official_title | Official title preferred for searching; brief title for display. |
| overall_status | RECRUITING · ACTIVE_NOT_RECRUITING · ENROLLING_BY_INVITATION · NOT_YET_RECRUITING · COMPLETED · TERMINATED · WITHDRAWN · SUSPENDED |
| phase | Pipe-joined for multi-phase (e.g. PHASE2|PHASE3). |
| lead_sponsor_normalized | Same normalize_applicant() as device_510k.applicant_normalized — enables equi-joins. |
| lead_sponsor_class | INDUSTRY · NIH · OTHER_GOV · ACADEMIC · INDIV — useful for filtering out academic-led trials when you only care about commercial activity. |
| conditions / interventions | Pipe-joined strings. Use LIKE '%dental%' for case-insensitive substring match. |
| locations_country | Pipe-joined unique countries from all trial sites. |
| enrollment_count | Actual enrolled (for completed) or estimated (for ongoing). Read with enrollment_type. |
| start_date / completion_date / last_update_posted | All DATE typed. Order by start_date DESC for "recently launched" views. |
| raw | Full v2 protocolSection as JSON — pull additional fields without re-ingesting. |
Pulled from NCBI E-utilities (esearch + efetch). Two query strategies running in series each refresh:
"medtronic"[Affiliation]).| Column | Notes |
|---|---|
| pmid | Primary key. NCBI's unique paper ID. |
| title / abstract | Full text, useful for keyword searches even outside the curated set. |
| journal / journal_issn | For "what journals does X publish in" or "what's the top venue for Y" queries. |
| pub_date / pub_year | Watch for future dates — PubMed sometimes indexes papers with print issue dates that haven't occurred yet. |
| authors | Pipe-joined "Lastname Initials" strings. Compact; full author objects in raw. |
| affiliations | Pipe-joined full affiliation strings. Use LIKE '%india%' for country/city matching. |
| affiliation_sponsors | Pipe-joined list of known sponsor names that matched. Empty for keyword-only matches. |
| mesh_terms | Major MeSH headings — most precise topical signal NCBI provides. |
| keyword_matches | Which curated keyword brought the paper in. NULL if matched only via sponsor. |
| provenance | sponsor · keyword · both — filter by this to keep narrow scope. |
| doi | When present, build full-text link as https://doi.org/{doi}. |
| Briefing | Trials view | Pubmed view | How matched |
|---|---|---|---|
| landscape | Top-20 players (sponsor) | Top-20 players (sponsor) | From `top_players()` ranked by 510(k)+PMA*2 weight |
| positioning | Cast (sponsor) + dental topical | Cast (sponsor) + dental keywords | COMPETITORS list normalized via normalize_applicant() |
| india | by_country("India") | by_country([India + 8 cities]) | locations_country / affiliations substring match |
| china | by_country("China") | by_country([China + 8 cities]) | same — Beijing, Shanghai, Shenzhen, … |
| europe | by_country([20 EU countries]) | by_country([20 EU countries]) | Germany, France, UK, Italy, Spain, Netherlands, … |
| singapore | by_country("Singapore") | by_country(["Singapore"]) | single-country |
| three_pillar | Pillar conditions (wearable, biosensor, mental health, …) | Pillar keywords (wearable biosensor, digital therapeutic, …) | Curated condition + keyword lists at module top |
| product_shortlist | BUY-list firms (sponsor) | BUY-list firms + SaMD topical keywords | BUY_LIST firm names normalized |
| whitespace | Whitespace conditions (POC, wearable, neonatal, pediatric, …) | Whitespace keywords | Curated lists in module |
PHASE3+ trials AND >30 papers in last 5y, your 510(k) reviewer will already have a clinical framework — your evidence package can be lighter. If both are sparse, you may be early enough that De Novo is on the table.
Both query modules are pure-read: they take a DuckDB connection + intent, return list-of-dicts ready for any briefing template or notebook. They tolerate missing tables (return [] if you query before the first ingest finishes) — so you can wire them into a briefing draft without breaking the render.
from fda_radar.analysis import trials as trial_q
# All trials sponsored by a sponsor cohort (normalized names)
trials = trial_q.trials_for_sponsors(con, ["medtronic", "boston scientific"], limit=50)
# Only active (RECRUITING / NOT_YET_RECRUITING / ACTIVE_NOT_RECRUITING / ENROLLING_BY_INVITATION)
active = trial_q.trials_for_sponsors(con, ["medtronic"], active_only=True)
# Trials with at least one location in one of several countries
eu = trial_q.trials_by_country(con, ["Germany", "France", "Italy"], limit=40)
# Trials with conditions matching ANY of several substrings
dental = trial_q.trials_for_conditions(con, ["dental", "caries", "oral health"], limit=30)
# Aggregate summary for a peer cohort — counts by status, top conditions, top phases
summary = trial_q.cohort_trial_summary(con, ["medtronic", "abbott medical", "boston scientific"])
# returns: {total, active, by_status, top_conditions, top_phases}
from fda_radar.analysis import pubmed as pub_q
# Papers whose affiliation_sponsors column matches a known sponsor cohort
papers = pub_q.papers_for_sponsors(con, ["medtronic", "boston scientific"], limit=30)
# Papers whose keyword_matches OR title OR mesh_terms contains a substring (any)
dental_ai = pub_q.papers_for_keywords(con, ["intraoral camera", "dental AI"], limit=30)
# Papers whose author affiliations contain a country/city substring
in_papers = pub_q.papers_by_country(con, ["India", "Mumbai", "Bengaluru"], limit=25)
# Monthly publication count per sponsor cohort (last N months) — "scientific velocity" signal
vel = pub_q.sponsor_publication_velocity(con, ["medtronic"], months_back=24)
# Top journals overall or within a topical slice
top = pub_q.top_journals(con, keyword_substrings=["wearable", "biosensor"], limit=15)
cd "Chrysalis Intelligence" && export UV_PROJECT_ENVIRONMENT=$HOME/fda-radar-local/venv
uv run python
>>> import duckdb
>>> con = duckdb.connect('/Users/michaelsappington/fda-radar-local/data/fda.duckdb', read_only=True)
>>> from fda_radar.analysis import trials, pubmed
>>> trials.trials_for_sponsors(con, ["dexcom"], active_only=True)
Both ingesters are wired into the standard pipeline. The daily fda-radar refresh picks them up automatically — no separate command needed.
| Endpoint | How often | Cost | Notes |
|---|---|---|---|
| clinical_trials | Daily incremental (lastUpdatePostDate watermark) | Free | No API key. ~3-5 min per refresh once the historical 5y is loaded. Page size 1000. |
| pubmed | Daily incremental (pub_date watermark) | Free, but rate-limited | 3 req/sec anonymous. Set NCBI_API_KEY in ~/fda-radar-local/.env for 10 req/sec (3× faster, no other change needed). Get a key at account.ncbi.nlm.nih.gov. |
# Trials only, narrow window
uv run fda-radar ingest --endpoint clinical_trials --start 2026-05-01 --end 2026-05-31
# PubMed only, narrow window
uv run fda-radar ingest --endpoint pubmed --start 2026-05-01 --end 2026-05-31
# Full incremental refresh (uses watermark - 14 day lookback)
uv run fda-radar ingest --endpoint clinical_trials
uv run fda-radar ingest --endpoint pubmed
The next time you run fda-radar refresh after this change, both endpoints will do a 5y backfill (no watermark exists yet). Realistic times:
NCBI_API_KEY is set: 2-4 hours with the key, 8-12 hours without. Sponsor-affiliation fan-out (500 esearch + N efetch per sponsor) dominates the runtime.The launchd daily refresh at 6:15 AM may extend by these amounts the first day. Subsequent runs are 14-day incrementals — back to ~10 min for trials, ~30 min for PubMed.
Both ingesters normalize sponsor names with the same normalize_applicant() the FDA tables use (lowercase, strip Inc/LLC/Ltd/Corp/GmbH/etc., collapse whitespace). That works for ~70-80% of obvious cases. It will miss:
When a firm appears empty in the trials/pubmed section but you know it's active, the name-match probably missed. Try the live ClinicalTrials.gov or PubMed UI to confirm.
etl/ingest_pubmed.py:MEDTECH_KEYWORDS for PubMed search expansionWHITESPACE_CONDITIONS in research/whitespace.py)The next refresh picks the new keywords up; existing data isn't re-tagged automatically.
PubMed's pub_date reflects the print issue date, which can be months in the future for accepted-but-not-yet-printed papers. You'll see records with pub_date in 2026-12 or even 2027. They're not corrupt — they're "in press". Treat them as current.
predicate_citations table).| Extension | What it adds | Effort |
|---|---|---|
| Trial outcomes ingest | Pull results when reported — primary outcome direction, statistical significance, safety events. Currently we only have protocol + status. | Medium — CT.gov v2 has results modules but reporting is sparse; needs cleanup logic. |
| Pre-print monitor | Add bioRxiv / medRxiv ingest for earlier signal on dental-AI / wearable / SaMD research before peer review. | Small — both have JSON APIs; ~3 hrs. |
| FDA AdComm coverage | FDA Advisory Committee meetings often discuss the same devices that surface in CT.gov / PubMed; cross-linking would be powerful for PMA-track briefings. | Medium — FDA AdComm calendars exist as RSS; record extraction needs parsing. |
| Citation-graph view | Which papers cite which other papers in a topical area — surfaces seminal works + emerging thought leaders. | Large — requires NCBI's PMC OAI-PMH or external citation databases (OpenCitations). |
| Per-sponsor scientific velocity dashboard page | The macro already supports a view='velocity' bar-chart for monthly publication count; not yet wired into any briefing. Easy to surface in landscape or positioning. | Small — <1 hr per briefing. |