create_watch
ChatGPTSave a watch so future sessions surface new matches automatically. Covers 'let me know when the M5 Mac mini ships', 'track mentions of Infisical', 'watch for anything about Stripe pricing'. Keyword watches (default) use PHRASE matching — the query words must appear adjacent and in order ('M5 Mac mini' matches 'the new M5 Mac mini' but not a roundup that merely mentions M5, Mac, and mini scattered apart), which keeps a watch quiet unless an article is really on-target. Word order matters, so a query phrased the way a headline would read ('OLED iPad mini', not 'iPad mini OLED') matches more. Tracking a concept across many phrasings is a semantic watch instead (kind='semantic', NEWSMIND MAX ONLY), which matches by meaning via embeddings and is order-insensitive. Scope is optionally narrowed to one feed (feed_id) or a folder (folder). For out-of-band alerts set delivery='email' or 'webhook' with a cadence; default delivery is in_session, surfaced via get_new_articles and get_watch_matches. Returns the new watch_id and a backfilled_matches count (existing in-window articles that already matched). Ambiguity signal. The response carries match_sources, the publishers this watch already matched. Unrelated publishers — a baseball statistics site, a funeral home and a local TV station all matching 'Brian Shaffer' — indicate several different subjects share the name, whereas many outlets covering one subject do not. The server deliberately does not classify this: publisher diversity alone cannot separate the two cases, and guessing wrong nags people whose watches are fine. require_terms narrows an ambiguous watch to a word only the intended story would carry, and is available on every plan. Semantic-watch calibration. The response also carries threshold_preview (cutoff → surviving match count), top_similarity, and a human-readable note. A backfill of hundreds or thousands of matches indicates a miscalibrated threshold rather than a productive watch. threshold accepts any value in (0,1], so the preview ladder is a guide rather than a fixed set of choices. Changing the threshold via update_watch re-runs the backfill and returns a fresh preview, so widening or narrowing is one call. Per-plan active-watch caps apply (Pro 20, Max 100).
delete_watch
ChatGPTRemove a saved watch and its match history — 'stop watching X'. Idempotent: a non-existent watch_id returns ok=false rather than throwing. Silencing a watch without deleting it is update_watch with active=false.
export_opml
ChatGPTExport the user's RSS subscriptions as an OPML 2.0 document — the standard portable format every RSS reader (Feedly, Inoreader, NetNewsWire, etc.) imports. Subscriptions are grouped by folder. Use when the user asks 'export my feeds', 'give me an OPML', 'I want to back up my subscriptions', or 'how do I move my feeds elsewhere'. Returns OPML XML as text the user can save to a .opml file. Works even if the subscription has lapsed — export is never paywalled.
get_account_info
ChatGPTReturn the caller's Newsmind account info — email, plan tier (pro/max/expired), subscription status, billing period end, trial end, feed limit (max feeds the plan allows), and current feed count. Answers questions about the account, plan, billing, trial, or feed quotas: 'who am I logged in as', 'what plan am I on', 'when does my trial end', 'what's my feed limit', 'how many feeds can I have', 'am I close to my quota', 'how many feeds do I have'. Read-only. Conditional response fields, each absent when there is nothing to report. feed_warnings appears only when one or more subscribed feeds have failed 3 or more consecutive fetches and returned no items for 48 hours (they are still retried); its shape is { failing_count, failing_samples: [{feed_id, title, url, last_error, diagnosis?}], failing_hosts?: [{host, feed_count, shared_error?}], note }, capped at five samples and ten hosts; the url is the address that stopped working, failing_hosts names every host failing 2+ of these feeds so a host-wide outage reads as one breakage rather than N, and the note says how to replace a moved feed. Full diagnostics are in get_feed_health. import_failures appears only when feed imports failed permanently in the last 7 days and carries its own message; the failed URLs and reasons are in get_import_status.
get_article
ChatGPTFetch the full content of a single article by ID, including read/starred status. Lazy-fetches via Mozilla Readability if the feed only had a summary (slower on first call; cached afterward).
get_article_counts
ChatGPTCheap overview: count articles grouped by feed, day, or hour. Returns small aggregate data, not the articles themselves. Use this BEFORE get_unread_articles or search_articles_by_keyword when the user asks 'how many', 'which feeds', 'when', etc. — much smaller token footprint than fetching articles. Also doubles as a feed-velocity check: get_article_counts(group_by="feed", since_hours=1) shows which feeds have been the noisiest recently — useful when one feed is dominating the queue and you're deciding whether to suggest excluding it via exclude_feed_ids on get_unread_articles / get_new_articles.
get_articles_by_feed
ChatGPTAll articles from one feed, newest first, regardless of read or starred state. Covers archival browsing — 'what did WIRED publish this week', 'show me everything from MacRumors'. Unread-only results for a single feed come from get_unread_articles with feed_id. Subject to tier retention (Pro 30d / Max 90d).
get_feed
ChatGPTOne feed's metadata — id, title, URL, folder, last poll time, error status. For all the user's feeds, use get_feeds. subscribed_at is when this user subscribed to this feed. It is NOT the article-visibility cutoff: what the user can see is bounded by tier retention (Pro 30d / Max 90d), which is usually wider, so an article older than subscribed_at can still be readable.
get_feed_health
ChatGPTPer-feed diagnostics for the user's subscriptions. Returns last successful poll, consecutive failure count, last error, last article date, and articles-per-week rate. Use this when the user asks 'which feeds are dead', 'which feeds are broken', 'which feeds haven't published lately', or wants to prune their subscription list. status is computed, and rows come worst first: 'failing' (3+ consecutive fetch errors and nothing delivered for 48 hours; it is still retried), 'empty' (the feed fetches fine but its document holds nothing that can ever reach the reader — either it has never contained a single item, or every item it does contain is stamped before syndication existed, which is what a placeholder page dated 1971-01-01 looks like; last_item_published_at carries that date. Neither shape starts working on its own; unsubscribe or find another source), 'no_items' (the feed delivered before, but every document fetched for 48+ hours has held no items; last_nonempty_at is the last time one did. Report that date, not a cause: a search feed limited to recent results is empty whenever nothing matched, while a site's main feed staying empty can mean the site moved or broke it), 'outside_window' (the feed does carry real, datable items, but the newest one is older than the user's retention window; last_item_published_at says how much older — report that date rather than guessing why), 'idle' (nothing stored inside the window and no verdict above applies yet: the feed answers 'not modified', its items carry no dates, they have not been stored yet, or it was added under 48 hours ago), 'stale' (the newest stored article is 30+ days old), 'healthy' (an article in the last 30 days). feed_id narrows to one feed. Erroring rows carry host_errors ({host, feeds_on_host, erroring_now, shared_error?, note}): a whole host erroring alike is the host, not the feed; a lone error is the feed.
get_feeds
ChatGPTList all RSS feeds the user is subscribed to. Returns id, title, URL, last poll time, and any error status. Erroring feeds carry host_errors: how the user's other feeds on that host did (count, and whether the message is shared).
get_import_status
ChatGPTReturn the status of the user's pending imports — from import_opml, or a subscribe_to_feed that answered queued. Call this when the user asks 'how is my import going', 'how many feeds are left to import', 'did all my feeds get added', or after import_opml returns a non-zero queued count. Reports pending (still waiting for the worker), succeeded (added in the last 24h), and failed (validation rejected — e.g. dead URL or non-feed) row counts plus a sample of the most recent failures. Also lists refused_subscribes (addresses subscribe_to_feed refused in the last 7 days), each rechecking (a further check is scheduled; added on its own if it answers), added or refused. Read-only.
get_new_articles
ChatGPTReturn articles published since this connection's last check, oldest-first, and atomically advance the per-token cursor. Serves 'what's new since I last asked?' across separate conversations: the cursor lives server-side per token, so nothing has to be remembered in the conversation. First call defaults to the last 24 hours. Pagination. has_more: true means articles remain. The cursor has already advanced past what was returned, so an identical repeat call returns the next batch; the set is fully drained when has_more is false. Peek mode. mark_seen=false returns articles without advancing the cursor, which leaves a batch re-returnable after a cancelled conversation or a transient error. The default mark_seen=true advances it. Filter. exclude_feed_ids skips one or more feeds — 'what's new minus Hacker News' when a firehose feed is dominating the queue. Conditional response fields, each absent when there is nothing to report. feed_warnings appears only when at least one subscribed feed has failed 3 or more consecutive fetches and returned no items for 48 hours (it is still retried); it carries failing_count, up to five failing_samples (each with the url that stopped working, round-robined across hosts so they span causes), failing_hosts when some host is failing 2+ of them ({host, feed_count, shared_error?}, biggest first — this is what tells you 42 of 43 failures are one dead host rather than 42 separate ones), and a note stating the state and how to replace a feed that has moved. Full diagnostics for those feeds are in get_feed_health. watch_summaries appears only when the user has saved watches, carrying watch_id, label, query, and new_match_count per watch; the matching articles themselves come from get_watch_matches. import_failures appears only when one or more feed imports failed permanently in the last 7 days and carries its own message; the failed URLs and reasons are in get_import_status. Re-stamped archives. An article may carry likely_archival ({title_year, matched_text, pattern}): its title states a date a year or more before its published_at, which happens when an aggregator re-indexes an old page under today's crawl date. The article is genuinely in the feed; its publication date is what is unreliable.
get_reading_history
ChatGPTReturn articles the user has marked read, most-recently-read first. Use when the user asks 'what have I read', 'show my reading history', 'what did I read about X last week'. Subject to tier retention — articles past the retention window are pruned and won't appear. feed_id narrows to one source; since_days narrows by read_at (when the user read it), not publish date.
get_starred_articles
ChatGPTReturn the user's starred articles, newest-starred first. Use when the user asks 'what have I starred', 'show my starred articles', 'what did I save this week', or wants to revisit favorites. Subject to tier retention — articles starred but past the retention window (Pro 30d / Max 90d) are not returned, since stars do not bypass retention. feed_id narrows to one source.
get_story_clusters
ChatGPTGroup redundant coverage in the user's feeds into clusters. Use when the user asks 'what's the big news', 'group my feed by story', 'what's everyone covering', 'am I subscribed to overlapping feeds', or to cut noise on a busy news day. Pick a matching strategy with `match`: - semantic (NEWSMIND MAX ONLY; default on Max) — same STORY across DIFFERENT outlets, matched by MEANING using article embeddings, so differently-worded coverage of one event clusters together (e.g. 'SpaceX files for IPO' from TechCrunch, The Verge, Engadget, and Ars). Best for 'what's the big news'. On other plans a semantic/both request runs exact instead and the response carries match_requested plus a note — relay the note rather than retrying. - exact (default on other plans) — the same ITEM literally syndicated into several feeds, matched by canonical URL or near-identical title. Best for 'do I have overlapping subscriptions' (HN front page + a HN-curated newsletter, NYT + an NYT topic feed). Cheaper than semantic and catches items with no embedding. - both — runs exact first, then semantic, and drops semantic clusters already fully covered by an exact one. Each cluster returns a canonical article (the outlet that published earliest) plus also_covered_by (the rest), along with the match kind that produced it. Clusters span at least 2 distinct feeds; singletons and single-feed repeats are dropped. Sorted largest-first. Semantic matching skips the small fraction of articles with no embedding; exact matching covers those.
get_unread_articles
ChatGPTGet unread articles, newest first, with slots shared across feeds so one high-volume source cannot fill the page. Covers 'what's new', 'what landed today', 'show me recent articles' — recency without a specific topic. since_hours narrows to a recent window (12 for 'since this morning', 24 for 'today'); feed_id narrows to one source; exclude_feed_ids drops one or more feeds, which matters when a high-volume feed dominates the queue. Takes no query, unlike search_articles_by_keyword. The cursor-based 'since I last asked' view across separate conversations is get_new_articles. Re-stamped archives. An article may carry likely_archival ({title_year, matched_text, pattern}): its title states a date a year or more before its published_at, which happens when an aggregator re-indexes an old page under today's crawl date. The article is genuinely in the feed; its publication date is what is unreliable. Topic clustering. lean: true with a high limit (up to 1000) returns the entire unread set in one compact response — titles and metadata without article bodies — which is the shape that fits a large queue into a single call for grouping into story threads. set_read_state with read: true and article_ids clears a thread in bulk. Engagement ranking. boost_engaged_feeds: true re-orders the recent unread pool by a per-feed engagement score, so feeds the user reads and stars rank above habitually-skipped ones, with recency as the tiebreaker. This answers 'what should I read' / 'what's worth my time' better than strict reverse-chronological order. Default false gives a pure newest-first timeline. Source balancing. Slots are shared out among the feeds that published recently, so a feed running 250 items a day cannot crowd out one running five. Only feeds with something recent take part — a dormant feed does not earn a slot by being stale. When this changed the answer the response carries balanced ({window_hours, max_per_feed, feeds_represented, capped_feeds}), naming the feeds that had more to give. balance_feeds: false restores strict newest-first; feed_id shows one feed in full.
get_unread_count
ChatGPTTotal unread count, scoped to subscribed feeds within tier retention (Pro 30d, Max 90d). Cheap — no article data. Pass by_feed: true to also get per-feed breakdown (which sources are noisy). Use BEFORE get_unread_articles when only a count is needed; great for 'how many unread' and 'where are my unread'.
get_watch_matches
ChatGPTPull the unseen matched articles for the user's watches (from watch_hits, newest-detected first). With watch_id, runs one watch; without, runs all and groups results by watch. By default marks the returned articles as seen so the next call only shows new ones; mark_seen=false peeks. Serves 'what's new on my watches', 'show me the M5 Mac mini matches', and follow-ups to the watch_summaries field on get_new_articles. Each article includes matched_at (when detected) and, for semantic watches, a similarity score. Archival matches. An article may carry likely_archival ({title_year, matched_text, pattern}): its title states a date a year or more before its published_at, which happens when an aggregator re-indexes an old page and re-stamps it with the crawl date. Demoted rows (archival or low-signal) sort after clean ones, and a row carrying both sorts last of all. The match is real and the article is real — the publication date is what is unreliable, and for a long-running story an old item can be exactly what the watch is for. The user's own exclusions. A match may carry user_excluded ({term}) — it matched one of the watch's exclude_terms. It is demoted rather than dropped, so an over-broad guess stays recoverable and the excluded term remains adjustable. require_terms filters at match time and reaches the backfill, so it removes a subject more thoroughly than exclude_terms does. Also carried elsewhere. A match may carry also_in, the OTHER feeds that ran the same story; the duplicate was collapsed into one alert rather than repeated. also_in lists the feeds that were collapsed AWAY, which may be the original publisher rather than the aggregator, so neither side is necessarily canonical. Repeated overlap between the same two feeds is what get_story_clusters(match='exact') reports on. Non-news sources. An article may carry low_signal ({source, rule}): Google News titles end 'Headline - Source', and this one is an encyclopedia entry, film database, statistics page, obituary, archive page, or a scraped video repost rather than reporting. Often it is a different person of the same name. These are not dropped, because roughly half of IMDb's matches here are real syndicated news. The flag describes the ITEM, not the outlet — rule: "obituary" comes from a headline rule that fires at ANY publisher, so source there may name a perfectly good newspaper that happened to run an obituary. The other rules are a short denylist of named non-news sites, so an unfamiliar small outlet is never flagged by those. It does not catch a wrong-person match from a genuine newsroom.
import_opml
ChatGPTBulk-subscribe to feeds from an OPML 2.0 document. Handles pasted OPML XML or a handful of feeds at once. Hard cap: 100 entries per call — larger imports (Feedly/Inoreader exports and the like) are handled by the file upload at https://newsmind.app/auth/account, which has no entry limit. Idempotent: already-subscribed feeds are skipped. Returns immediately after format-validating and queueing; actual fetching runs in the background and progress is reported by get_import_status, which gives live pending/succeeded/failed counts. Do not quote a completion time to the user: the queue is shared across customers and most of the wall-clock is the publishers' own servers responding, so counts are the honest answer.
list_watches
ChatGPTReturn the user's saved watches with scope, delivery, active state, and current unseen-match counts. Use when the user asks 'what am I watching', 'show me my watches'. Read-only — does NOT mark anything seen.
move_feed_to_folder
ChatGPTReorganize an existing subscription into a folder. Omit folder to remove the feed from any folder (top-level). Does not affect the feed's articles, visibility window, or unread queue.
report_issue
ChatGPTFile a bug report, feature request, or feedback to the Newsmind team. Use this when the user expresses frustration with how a tool worked (a discovery returned nothing they expected, an action didn't do what they intended, an error message was confusing), asks for a feature that doesn't exist, or volunteers feedback they want the developers to see. Include reproducer details: what they asked, what tool you called, what came back, what they expected. Don't include the user's API token, JWT, or any other credential — the server scrubs them defensively but reports should be clean. Returns an issue ID the user can reference if they email support.
search_articles_by_keyword
ChatGPTFull-text keyword search across all subscribed articles. Returns {articles: [...], fallback?: 'or', hint?: string, retention: {window_days, cutoff_at, cutoff_hit?}}. Articles are ranked by keyword relevance with a 14-day recency half-life — best matches first, with a mild bias toward recent articles. Requires a non-empty query; newest-first ordering with no topic is get_unread_articles. Compound-query handling: when an AND search ('Apple battery EU AI regulation') returns no articles, the tool retries with OR semantics and tags the response fallback='or', meaning the result set is less precise than the query asked for. hint is a human-readable note explaining what happened when the result set is empty or fell back. Retention field — retention.window_days is the caller's tier cap (Pro 30, Max 90); retention.cutoff_at is the ISO date past which nothing is stored. retention.cutoff_hit: true means retention likely bounded the result set: since_days exceeded the window, a multi-term query came back empty, or the oldest match sits at the wall. Several narrow single-concept queries return more precise results than one broad multi-concept query. Paraphrase and concept matching, where keyword overlap is unlikely, is search_articles_by_meaning on the Max plan. Scope field — when the caller's feeds hold more articles than one search can rank, the response carries search_scope {since, matches_ranked, note}: the rank was chosen among the matches published since since, and older matches were not ranked. Surface it when present; a more specific query reaches further back.
search_articles_by_meaning
ChatGPTConceptual / semantic search across all subscribed articles. Matches an idea, theme, or concept that need not appear as exact keywords (e.g. 'unintended consequences of AI regulation', 'companies pivoting away from open source'). It covers compound or paraphrased questions where keyword overlap with the article text is unlikely — 'GPU bottleneck' matches articles about 'AI chip supply', 'frontier safety' matches 'AGI alignment' — which keyword search by construction cannot. Returns {query, count, articles, retention: {window_days, cutoff_at, cutoff_hit?}}, ranked by meaning rather than exact word match. The response reports embedding coverage; during an indexing drain (fresh upgrade, newly added feed) it may carry a 'still indexing N of M' note — and still_indexing: true on an empty result, which means 'not yet', not 'no matches'. retention carries the Max 90-day cap and a cutoff_hit flag, set when the retention wall likely bounded the results — the relevant case for 'go back as far as possible' style questions. Included in the Newsmind Max plan only; calls from other plans return a max_required error, and keyword search remains available on every plan as search_articles_by_keyword. Scope field — when the caller's feeds hold more articles than one search can read, the response carries search_scope {since, articles_searched, note}: only articles published since since were searched by meaning. Surface it when present.
set_read_state
ChatGPTMark articles read or unread. read picks the direction (true = read, false = back to unread). Scope is EITHER article_ids OR through_published_at — pass exactly one. By ID — article_ids: [1,2,3]. Use when the user is responding to a list you just showed them: 'mark the first three', 'mark everything you just listed', or 'I read that by accident, put it back' (with read: false). By high-water mark — through_published_at (ISO 8601). Every subscribed article published at or before that timestamp flips. When the user says 'mark all read' or 'I'm caught up', pass the CURRENT UTC time — articles arriving later stay unread, so there's no race window. With read: false this is the bulk undo for 'put everything back to unread'. Add feed_id to scope to one source ('mark all from MacRumors as read'). Read state and stars are independent — this never touches a star.
set_starred
ChatGPTStar or unstar articles (saved/favorited). starred: true adds the star, starred: false removes it. Operates on the stars table only — read state is untouched. Re-starring an already-starred article is a no-op and does not bump its starred_at.
subscribe_to_feed
ChatGPTAdd an RSS/Atom feed to the user's subscriptions. The feed will be polled automatically. url may be either the actual feed URL or a regular site/homepage URL — if the URL returns HTML, the server auto-discovers the feed: the <link rel="alternate"> head tag first, then feed-looking or RSS-labelled links in the page body, so https://stratechery.com/ works as well as the .xml address. If the page advertises nothing, the conventional locations (/feed, /rss.xml, /atom.xml, …) are probed before giving up. When auto-discovery fires, the response includes auto_discovered: { input_url, discovered_url }, recording what was supplied and what was found. When it fails. The result is { error: "invalid_feed_url", reason, message, suggestion, retryable, tried?, next_steps }. message and suggestion are human-readable, not raw diagnostics. tried lists the feed addresses already probed on the user's behalf, so those addresses are known-bad and re-probing them yields nothing new. next_steps is a machine-readable hint about what would resolve the failure: try_url carries a different, not-yet-probed URL; retry_later marks a transient failure that may succeed later; import_opml marks a case better served by an OPML export from the user's current reader; ask_user carries the question whose answer is needed to continue; get_import_status with after_seconds means the address is re-checked on its own: don't re-add it by hand; call that tool after the delay. reason values: not_a_feed, discovery_failed, http_error (with http_status), blocked_host, dns, timeout, unreachable, server_error, malformed_feed, empty_feed, not_a_url, private_address, unknown. When it is queued. A few addresses are checked on a schedule rather than inline, and the response is { status: "queued", feed_id: null, url, message }. Nothing is subscribed yet: tell the user it will appear within a few minutes, and follow it with get_import_status, which reports it as added or says why it could not be. Asking again while it is queued returns the same answer. Subscribing immediately surfaces the feed's in-retention backlog: the unread queue shows articles already in storage within the caller's window (30 days Pro / 90 days Max) rather than staying empty until the next poll. Anything older than that window stays hidden. Re-subscribing to a feed the user already has is a no-op; changing an existing subscription's folder is move_feed_to_folder.
unsubscribe_from_feed
ChatGPTRemove a feed from the user's subscriptions. Does not delete shared articles. Takes the feed's numeric id (from get_feeds or any article's feed_id); the feed's exact title or URL is also accepted and resolved against the user's subscriptions. If nothing matches, the response has status 'not_found' — the authoritative list of ids and titles is get_feeds.
update_watch
ChatGPTModify a saved watch without recreating it: pause or resume (active=false/true — 'mute my M5 watch'), rename, edit the query, change scope, switch delivery channel or cadence, or tune a semantic watch's threshold. Only the fields passed are changed. Editing the query, the threshold, or the SCOPE (feed_id/folder) rebuilds the match set; the response reports which via rebackfilled_because and how many via rebackfilled_matches ('re-pointed at the narrower feed; 4 matches instead of 9'). Re-pointing a watch at a narrower feed is the most direct fix when a watch matches the wrong subject rather than simply too much. zeroed_by is present when the edit emptied a watch that previously had matches, which is usually an over-narrow edit rather than the intended result. cadence_inert is present when a cadence was set on an in-session watch, where it schedules nothing — cadence only applies to email and webhook digests — and carries its own message. For semantic watches the response returns a fresh threshold_preview, so a stricter cutoff can be picked from data rather than guessed. Returns the updated watch.