# feedql: how to use it (for people and LLM agents) feedql is a personal feed and knowledge base that builds itself. It collects from sources a person chooses (Hacker News, Reddit, GitHub, X, Telegram, YouTube, forums, app-store reviews, registries, any website), removes duplicates, groups reports of the same event, scores each item against the person's **topics**, and always links to the **original source**. People read it on the web; programs and AI agents read and manage it through **MCP** or a **REST API**. Everything here is one account's own data: a key sees only its owner's topics and items. This page is generated from the server's own tool registry and route table, so the tool and route sections cannot drift from the code. `tools/list` over MCP returns the same tools with their complete JSON schemas. - Web app: https://feedql.app (sign in, Settings, API keys) - MCP (Claude Desktop, claude.ai, Claude Code, any MCP client): https://mcp.feedql.com (also https://api.feedql.com/mcp) - REST API: https://api.feedql.com/api/v1 - This page: https://feedql.com/llms-full.txt (also https://api.feedql.com/llms-full.txt, which then links the site pages); index: https://feedql.com/llms.txt Nothing on this page is a secret. Never paste a real key into a prompt, a URL, a log or a commit: keep it in an environment variable (`FEEDQL_KEY` below). ## 1. Quick start 1. Get access (section 2): an OAuth connector for Claude Desktop / claude.ai, or an `fq_` API key for scripts and Claude Code. 2. Find the topics: MCP `list_topics`, or `GET https://api.feedql.com/api/v1/topics`. 3. Read: MCP `search_items` with an FQL query such as `topic:golang since:1d score>=60`, or `GET https://api.feedql.com/api/v1/items?q=topic%3Agolang%20since%3A1d%20score%3E%3D60`. 4. Optional: act on what you read (vote, mark read, save, note, create or change topics). This needs a **write** key, and marking items works over REST and MCP. 5. If something is missing, wrong or broken, report it with `send_feedback` (section 10, [Report problems](#10-report-problems)). ```bash export FEEDQL_KEY=fq_... # made in feedql.app > Settings > API keys curl -s -H "Authorization: Bearer $FEEDQL_KEY" "https://api.feedql.com/api/v1/topics" ``` ## 2. Connect and authenticate | You are | Use | How | |---|---|---| | Claude Desktop or claude.ai | OAuth connector | Settings > Connectors > Add custom connector, URL `https://mcp.feedql.com`, Connect, sign in to feedql, choose **Read only** or **Read and manage**, Allow. No key is copied around. | | Claude Code | OAuth, or an API key | `claude mcp add --transport http feedql https://mcp.feedql.com` then `/mcp` to sign in; or with a key: `claude mcp add --transport http feedql https://api.feedql.com/mcp --header "Authorization: Bearer $FEEDQL_KEY"` | | A script, cron job, another agent runtime | API key `fq_...` | `Authorization: Bearer fq_...` on every request, REST and MCP alike | | A data pipeline reading one stream | Channel key `fqc_...` | same header; works only on `/c//...` and on `/mcp` (where it sees just that channel's tools) | | A generic MCP client with OAuth support | OAuth 2.1 | discovery below | **Scopes.** An API key or OAuth grant is **read** or **write** ("Read only" / "Read and manage"). Read lists and searches. Write also creates, changes and deletes topics, votes, marks read, saves, writes notes, manages webhooks, exports and channels. A read key simply does not see write tools in `tools/list`; a REST write route answers `403 forbidden`. Prefer a read key unless the task needs to change something. **Channel keys** (`fqc_...`, read or write, optional expiry) belong to one channel (a separately configured stream for a program); any other route answers `403`. **Account keys** (`fq_...`) read all of the owner's channels. The person makes and revokes keys in the app (Settings > API keys; channel keys on the channel page). A write connection can also make a key for a script with `create_api_key` (never above its own scope, 1 to 365 days, default 30, the secret is in the answer **once**; at most 10 live keys and 5 MCP creations an hour) and end one with `revoke_api_key` (not the key making the call). Read connections and channel keys cannot. Such keys show as made by MCP in Settings. Connected OAuth apps are listed next to the keys and can be disconnected there. **OAuth discovery** (MCP authorization spec 2025-06-18, PKCE S256 only, public clients): `GET /.well-known/oauth-protected-resource` and `/.well-known/oauth-authorization-server` on `mcp.feedql.com` or `api.feedql.com`; dynamic registration `POST /oauth/register`; `GET /oauth/authorize`; `POST /oauth/token` (`fqo_` access tokens, rotating `fqr_` refresh tokens). A call without a valid token gets `401` with `WWW-Authenticate: Bearer realm="feedql", resource_metadata="https://api.feedql.com/.well-known/oauth-protected-resource"`. A revoked or expired key is `401` too. ## 3. The data model in five lines - A **topic** says what to follow: a title, an optional **About** (`about`, up to 600 characters, in the person's own words; the scoring model reads it), a keyword pack, places and languages, and the **sources** it follows. `status` is `expanding` while the model builds the pack, then `ready`; `following: true` once followed. - An **item** is one report from a source. It has a `score` (0-100, how well it fits the topic, `null` until scored or in raw topics), a `rationale`, the original `url`, and may belong to an **event** (other reports of the same story). The same item id is used everywhere (MCP, REST, webhooks). - The **feed** is newest first and applies each topic's score threshold, the person's votes and their don't-show / must-see rules. - **Marks** are the person's own state on an item: `mine` = `{vote: up|down, read, saved, not_this, note}`. A down vote or "not this" teaches the topic by similarity of meaning. Only vote when the person decided. - **Channels**: the person's reading feed is the `main` channel. Extra channels are separate data streams for programs. Most agents only ever touch `main`. `list_topics`, `search_items`, `topic_feed` and `new_items` work on `main`; for another channel use `list_channels`, then `channel_items` / `channel_new_items` / `channel_stats` with its `channel` id or slug (the FQL key `channel:` is something else: an alias of `place:`). Items are kept for `retention_days` (365 by default) from collection (`kept_until` on each item); saved or noted items stay. ### The item payload The same object in MCP results, REST lists and webhooks: ```json { "id": "6720f1c2a5b7d9e8f0a1b2c3", "url": "https://example.com/original-article", "title": "Go 1.27 released", "summary": "feedql's own summary, in the account's summary language", "source": "hackernews", "kind": "post", "place": "r/golang", "lang": "en", "author_hash": "9f2c...", "published": "2026-10-11T07:30:00Z", "topic": {"id": "6720e0...", "title": "golang"}, "score": 82, "rationale": "why it matters", "metrics": {"points": 321, "upvotes": 321, "comments": 87}, "entities": [{"type": "domain", "value": "go.dev"}], "event": {"id": "...", "reports": [{"id": "...", "title": "...", "url": "...", "source": "...", "published": "..."}]}, "mine": {"read": true}, "kept_until": "2027-10-11", "seq": 1207 } ``` - `url` is always the original address: **link to it when you show an item**. `text`, `excerpt`, `image(s)`, `transcript_*`, `key_points`, `visibility`, `feedback`, `filtered_by`, `similarity`, `fields` appear when they apply (`get_item` has the most). A YouTube video's own description is its `text`; the spoken words come only when you ask: `transcript=text` (`transcript.text`) or `transcript=segments` (`transcript.segments`, each with `t` seconds) on `search_items`, `get_item`, `new_items`, `topic_feed` and the REST item routes (`?transcript=`), up to `transcript_max` characters (default 20000, at most 100000) with `transcript_truncated`, independent of `summary`, at most 20 rows of a page. A video from the person's own History or Watch later has its summary written as soon as the transcript is ready. - `summary_status` (`ready`, `pending`, `failed`): summaries are written when an item is first asked for. `pending` means one was just queued: call `get_item` again in a few seconds. - `metrics` always has `points`; `rating`, `stars`, `score`, `upvotes`, `comments`, `views`, `version` only when the source gave them. - `score` already includes the person's votes. `mine` is absent when the person did nothing with the item. - `kind`: post, review, app, release, issue, repo, video, article, notice, tender, grant, job. `place`: `r/`, `owner/repo`, `@`, `/`, `@handle` or a website host. ## 4. MCP **Endpoint**: `https://mcp.feedql.com` (or `https://api.feedql.com/mcp`). Streamable HTTP, stateless, JSON responses, bearer authentication. A client library or `claude mcp add` handles the protocol. By hand, each call is one POST: ```bash curl -s https://api.feedql.com/mcp \ -H "Authorization: Bearer $FEEDQL_KEY" \ -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search_items","arguments":{"query":"topic:golang since:1d"}}}' ``` No `initialize` handshake is needed: the server is stateless, every POST stands alone. The `Accept` header is required by the protocol; the reply is always plain JSON, never an event stream. The answer is `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":""}],"structuredContent":{...}}}`; `structuredContent` and the text are the same value. A key that cannot use a tool does not see it, and calling it is an error. `tools/list` returns every tool the key may use with full input and output schemas; `prompts/list` the prompts. **Failures** are tool errors (`result.isError: true`) whose text is JSON: `{"error":{"code":"bad_request","message":"...","field":"item_ids","retry_after_seconds":3}}` (`field` and `retry_after_seconds` only when they apply). Codes: `bad_request` (with `field`), `unauthorized`, `forbidden`, `not_found`, `conflict`, `limit_reached`, `rate_limited` (wait `retry_after_seconds`), `unavailable` (retry later), `query_syntax` (the FQL is wrong; the message has the position), `query_unsupported`, `template_quota` (the account already has a live channel of that template; the message says which). Arguments that break a tool's schema come back as `bad_request` too. **Rate limit**: a bucket of 120 calls refilling over a minute per key; a write call costs 4. Over it: `rate_limited`. This bucket is separate from the REST one (section 5): mixing REST reads with MCP writes spends each from its own. **Long work never blocks a call.** Building a keyword pack (`create_topic`), a test run (`start_test_run`), an export (`start_export`) and the first use of a meaning search return at once; poll the status tool (`get_topic`, `get_test_run`, `get_export`, or repeat the same `search_items`) every 2 to 5 seconds. **Paging.** A list holds up to 30 items. List tools return `next`; pass it back as `cursor` (with the same query) for older items. It is absent on the last page. A page can be short, even empty, while `next` is present (the search scans in bounded rounds): stop only when `next` is absent. Pass the cursor as it is (URL-encode it in a REST query string), and deduplicate by item `id`: an item scored while you page can show up twice. `new_items` is different: call it once without a cursor to get "now", then with the `cursor` it returned; items come oldest first, `more` says call again at once. **Language.** Tools that return items take `lang` (`en`, `uk`) for summaries and translated titles; default is the account's. ### Tools 129 tools are offered to a write key; a read key sees the ones marked "read key". "Call" is a ready `tools/call` params object; "Result" shows the shape of `structuredContent` (types, not values; `{...}` is collapsed; the item payload is described above). ### Domain: feed #### `credibility_check` *Is this post worth my attention* (read key) Check one item for clickbait, yellow press, satire and thin posts. Returns a card: an overall label (worth_reading, thin, clickbait, doubtful) with one sentence why, then rows that each say where they came from (from: the tools whose data they use) and mark what the model judged (model_judgement) apart from stored facts: whether the title is delivered by the text, the sensational phrases quoted from the post, whether the post names its source, how many reports from how many sources feedql holds for the event (a … Arguments: - `item_id` (string, required): the item to check - `lang` (string): language of the labels (en, uk); default is the user's summary language Call: `{"name":"credibility_check","arguments":{"item_id":""}}` Result: `{"item_id": "string", "label": "string", "label_text": "string", "why": "string", "rows": [{…}], "cross_checked": false, "probabilities": {"": 0.0}, "offers": ["string"], "beta": false, "cached": false, "version": "string"}` #### `feed_sources` *Sources in the feed* (read key) List the sources present in the feed window (the recent entries a topic's feed ranks; all followed topics without topic_id) with how many entries each reported, most first: source is the code to use as source: in search_items or as source in topic_feed (ext:x and ext:linkedin are posts the user's own browser extension read), label is its name. Arguments: - `topic_id` (string): only this topic's feed window (optional; default all followed topics) Call: `{"name":"feed_sources","arguments":{"topic_id":"6720e0a1b2c3d4e5f6a7b8c9"}}` Result: `{"sources": [{…}]}` #### `get_item` *One item in full* (read key) Fetch one item with its full text, entities, score and rationale, and the other reports of its event. summary_status is ready, pending, failed or none (a short post: its own text is the whole story, no summary is written) for the summary in the call's language: summaries are written when an item is asked for, so pending means one was just queued, call get_item again in a few seconds. Arguments: - `id` (string, required): the item id - `lang` (string): language of the title and summary, an ISO 639-1 code (en, uk, de, fr, es, pl and more; an unknown code answers with the list); … - `summary` (boolean): true: every item carries feedql's summary in lang (one not written yet is started, summary_status says pending; at most 25 start … - `transcript` (string): none (default), text or segments: a YouTube video carries its transcript (the spoken words, from captions) in transcript.text, or … - `transcript_max` (integer): characters of transcript per item, 1 to 100000 (default 20000); only with transcript text or segments Call: `{"name":"get_item","arguments":{"id":"6720f1c2a5b7d9e8f0a1b2c3"}}` Result: `{"id": "string", "url": "string", "title": "string", "original_title": "string", "title_original": "string", "translation": {"lang": "string", "state": "string", "confidence": 0.0, "quality": "string"}, "lang_original": "string", "translated": {"from": "string", "conf": 0.0, "low": false}, "text": "string", "body": "string", "body_truncated_from": 0, "summary": "string", "summary_status": "string", …` #### `item_check_log` *How we checked this item* (read key) Read, from stored data, how feedql checked the event an item belongs to, and every retelling of that event. `proof` is the short summary readers see under the item's text: `state` (published: a verdict is written; draft: a proposal waits for a person and is not a verdict, no label; running; no_verdict with the check's `status`; none: no check was run; not_needed: triage found nothing to fact-check), up to three `claims` with their written label, `independent_sources`, `original_sources` and when. Arguments: - `item_id` (string, required): the item id; any report of the event works - `topic_id` (string): the topic the item was seen in (optional) - `lang` (string): language of the titles (en, uk); default is the user's summary language Call: `{"name":"item_check_log","arguments":{"item_id":""}}` Result: `{"item_id": "string", "check": {"status": "string", "method": "string", "started_at": "string", "energy": {…}, "steps": [{…}], "queries": [{…}], "sources": [{…}], "others_checks": [{…}], "matches": [{…}], "unrelated_pages": 0, "verdicts": [{…}], "gaps": [{…}], "original_url": "string", "page_url": "string", "archived_url": "string", "complaints": 0, "my_complaints": {"": "string"}}, …` #### `item_thread` *The thread an item is part of, and why* (read key) Story 37.33, read only, from stored data: the technical thread(s) an item belongs to (up to three) and the reasons shown for them. A thread joins every report about one project, product, library, model or app across sources (sites, Hacker News, Reddit, GitHub, YouTube, X, forums) and months; each has `name`, `type`, `items` (and `about`, the ones about the subject itself; the rest are made for or run on it), `events`, `by_source`, `first`, `last`, `by_month` and a short `timeline` (the first report, releases, the … Arguments: - `item_id` (string, required): the item id - `topic_id` (string): the topic the item was seen in (optional) Call: `{"name":"item_thread","arguments":{"item_id":""}}` Result: `{"item_id": "string", "threads": [{…}], "why": [{…}]}` #### `item_timeline` *What happened next to this story* (read key) Story 37.30, read only, from stored data: the debunks, corrections, developments, confirmations, follow-ups and series incidents linked in time to the story an item belongs to. `timeline` lists lines with `type` (debunk: a fact-checker's or our own written verdict that calls a claim false or misleading; correction; development; confirmation; follow_up; series: another incident of the same running series, e.g. Arguments: - `item_id` (string, required): the item id; any report of the event works - `topic_id` (string): the topic the item was seen in (optional) - `lang` (string): language of the titles (en, uk); default is the user's summary language Call: `{"name":"item_timeline","arguments":{"item_id":""}}` Result: `{"item_id": "string", "timeline": [{…}]}` #### `new_items` *Items since a cursor* (read key) Fetch what matched or was scored since a cursor, oldest first. Call without a cursor first to get the cursor of now, then pass the returned cursor each time; while more is true call again. Reading never changes what the user sees as read. include=filtered also returns the items a topic in hide exclusion mode kept out (each has filtered_by: exclusion: or filter:, and no score). Arguments: - `topic_id` (string): only this topic (optional) - `include` (string): filtered: also return the items a topic in hide exclusion mode kept out (filtered_by says why: exclusion: or … - `cursor` (string): the cursor of the previous answer; omit it the first time to get the cursor of now - `lang` (string): language of the title and summary, an ISO 639-1 code (en, uk, de, fr, es, pl and more; an unknown code answers with the list); … - `summary` (boolean): true: every item carries feedql's summary in lang (one not written yet is started, summary_status says pending; at most 25 start … - `transcript` (string): none (default), text or segments: a YouTube video carries its transcript (the spoken words, from captions) in transcript.text, or … - `transcript_max` (integer): characters of transcript per item, 1 to 100000 (default 20000); only with transcript text or segments - `window` (string): optional: only items inside this freshness window (auto, 1d, 3d, 7d or all) by publication time; by default new_items is a sync … Call: `{"name":"new_items","arguments":{}}` Result: `{"items": [{…}], "cursor": "string", "more": false, "retention_days": 0}` #### `search_items` *Search the feed (FQL)* (read key) Search the user's feed with a query in the feedql query language (FQL); scope collected or both also searches everything feedql collected from public sources (counts of both, items not in the feed; see scope). To follow a query, create_topic with its words as the title. (FQL syntax: section 6 of this page.) include=filtered also returns the items a topic in hide exclusion mode kept out (each has filtered_by: exclusion: or filter:, and no score). Arguments: - `group` (string): lay the page out in groups, a comma list of day, bucket, topic (groups[] in the answer: {day, bucket, topic, items}); an event … - `order` (string): order inside each group: severity (most serious, official first, newest), time or score; default the page order - `collapse` (string): event: one entry per event in the groups, the lead report with event_count - `tz` (string): IANA zone the days are cut in, e.g. Asia/Colombo; default UTC - `query` (string): an FQL query (see the tool description); empty lists the whole feed - `include` (string): filtered: also return the items a topic in hide exclusion mode kept out (filtered_by says why: exclusion: or … - `cursor` (string): the next value of the previous answer, to get older items (or the next part of a sorted ranking) - `lang` (string): language of the title and summary, an ISO 639-1 code (en, uk, de, fr, es, pl and more; an unknown code answers with the list); … - `summary` (boolean): true: every item carries feedql's summary in lang (one not written yet is started, summary_status says pending; at most 25 start … - `transcript` (string): none (default), text or segments: a YouTube video carries its transcript (the spoken words, from captions) in transcript.text, or … - `transcript_max` (integer): characters of transcript per item, 1 to 100000 (default 20000); only with transcript text or segments - `limit` (integer): rows in the page, 1 to 200; default 30 - `scope` (string): feed (default): the user's own feed only; collected: everything feedql collected from public sources, matched by any topic or by … - `sort` (string): points (the source's own score: HN points, Reddit score), comments, engagement (points + comments) or relevance (the score the … Call: `{"name":"search_items","arguments":{"query":"topic:golang since:1d score>=60"}}` Result: `{"items": [{…}], "next": "string", "status": "string", "retry_after_seconds": 0, "message": "string", "retention_days": 0, "truncated": false, "groups": [{…}], "counts": {"feed": 0, "feed_more": false, "collected": 0, "collected_capped": false, "collected_not_in_feed": 0}, "collected": {"available": false, "reason": "string", "items": [{…}], "next": "string", "ignored": ["string"]}}` #### `similar_items` *Items like this one* (read key) Find items similar in meaning to one item among the user's own history (semantic search, Story 4.7): up to 10 rows over the last 1-90 days, each with the AI score or the free signal, source, topic, publication time and the cosine similarity. Only items the user already has; never another user's data. Arguments: - `item_id` (string, required): the item to find neighbours of - `days` (integer): how far back to look, 1 to 90; default 30 - `limit` (integer): how many items, 1 to 10; default 5 - `lang` (string): language of the title, an ISO 639-1 code (en, uk, de, fr, es, pl and more); the title is the translation or empty with … Call: `{"name":"similar_items","arguments":{"item_id":""}}` Result: `{"items": [{…}], "note": "string"}` #### `source_info` *What a source is to me* (read key) A source as this user experiences it, from their own last 30 days: how many items it gave in 24 hours and 7 days (rate per day), the time of its last item, its health (ok, quiet, silent), the user's own votes on its items (up, down, hidden, read, saved) and the topics that received items from it. The source id is a code from feed_sources. Arguments: - `source_id` (string, required): one source code from feed_sources (hackernews, reddit, github, site:dou.ua) Call: `{"name":"source_info","arguments":{"source_id":""}}` Result: `{"source": "string", "label": "string", "posts_7d": 0, "posts_24h": 0, "per_day": 0.0, "last_post": "string", "health": "string", "my_votes": {"up": 0, "down": 0, "hidden": 0, "read": 0, "saved": 0}, "my_topics": ["string"]}` #### `summarize_event` *What all reports of the event say* (read key) Read every report of the event an item belongs to: feedql's neutral summary of the event (empty until written; summary_written says), and each report's title, link, source and time. Use it to summarise or compare how sources reported the same thing. Arguments: - `item_id` (string, required): any report of the event - `lang` (string): language of the title, an ISO 639-1 code (en, uk, de, fr, es, pl and more); the title is the translation or empty with … Call: `{"name":"summarize_event","arguments":{"item_id":""}}` Result: `{"item_id": "string", "event_id": "string", "summary": "string", "summary_written": false, "reports": [{…}], "sources": ["string"]}` #### `thread_get` *A technical thread* (read key) Story 37.33, read only: one thread - the subject (`name`, `type`), how many items (`items`, `about`, `events`), counts `by_source` and `by_kind` and `by_month`, the first and last report, the `timeline`, the `why` reasons (see item_thread) - and one page of its items newest first (`page`: title, source, kind, place, link to the original, date, role). Pass `cursor` from the answer as `before` for the next page. The same for every reader: it is built from public sources, never from anyone's private items. Arguments: - `thread_id` (string, required): the thread id (the subject's compact name, e.g. comfyui); find it with threads_search or item_thread - `before` (string): the cursor the previous page returned (omit for the newest items) - `limit` (integer): items per page, 1 to 100 (default 30) Call: `{"name":"thread_get","arguments":{"thread_id":""}}` Result: `{"id": "string", "title": "string", "name": "string", "type": "string", "items": 0, "about": 0, "events": 0, "by_source": {"": 0}, "by_kind": {"": 0}, "first": "string", "last": "string", "by_month": {"": 0}, "timeline": [{…}], "shown": false, "updated_at": "string", "why": [{…}], "related": [{…}], "page": [{…}], "cursor": "string"}` #### `threads_search` *Find technical threads* (read key) Story 37.33, read only: threads whose subject name starts with `query` (an empty query lists the biggest), each with counts by source, first and last report and a short timeline. Only threads with at least five items exist. Nothing is written. Arguments: - `query` (string): the start of a subject's name (comfy, n8n, claude); empty lists the biggest threads - `limit` (integer): threads, 1 to 50 (default 20) Call: `{"name":"threads_search","arguments":{}}` Result: `{"threads": [{…}]}` #### `topic_feed` *Read a topic's feed* (read key) Read one topic's feed, newest first, as the app shows it (score threshold applied). Pass the returned next as cursor for older items. Each topic shows only a freshness window (get_topic window: auto by volume with strong scores kept longer, 1d, 3d, 7d or all); older_items says how many more the window holds back, window=all or older=1 reads them. Arguments: - `group` (string): lay the page out in groups, a comma list of day, bucket, topic (groups[] in the answer: {day, bucket, topic, items}); an event … - `order` (string): order inside each group: severity (most serious, official first, newest), time or score; default the page order - `collapse` (string): event: one entry per event in the groups, the lead report with event_count - `tz` (string): IANA zone the days are cut in, e.g. Asia/Colombo; default UTC - `topic_id` (string, required): the topic id from list_topics - `cursor` (string): the next value of the previous answer, to get older items - `lang` (string): language of the title and summary, an ISO 639-1 code (en, uk, de, fr, es, pl and more; an unknown code answers with the list); … - `summary` (boolean): true: every item carries feedql's summary in lang (one not written yet is started, summary_status says pending; at most 25 start … - `transcript` (string): none (default), text or segments: a YouTube video carries its transcript (the spoken words, from captions) in transcript.text, or … - `transcript_max` (integer): characters of transcript per item, 1 to 100000 (default 20000); only with transcript text or segments - `source` (string): only items reported by these sources, comma separated codes from feed_sources (hackernews,reddit,ext:linkedin); default all - `sort` (string): fresh (newest first) or score (highest score first); default is the app's ranked order. Pass the same sort with every cursor - `window` (string): how far back to read, overriding the topic's own setting for this read: auto (by volume; strong scores stay longer), 1d, 3d, 7d … - `older` (integer): extra windows to reach back beyond the topic's window (what the app's Show older does); the answer's older_items says how many … Call: `{"name":"topic_feed","arguments":{"topic_id":"6720e0a1b2c3d4e5f6a7b8c9"}}` Result: `{"items": [{…}], "next": "string", "retention_days": 0, "truncated": false, "groups": [{…}], "older_items": 0}` #### `why_shown` *Why is this item in my feed* (read key) Explain from stored data, not from memory, why one item is shown to the user: the topic and its mode (raw: no AI score, a free signal; scored: an AI score against the topic's lowest score; must: a must-see entry), the score or the signal and its parts (reports, freshness, vote adjustment), the lowest score and whether the item passed it, the topic words and phrases found in the item, the exclusion words that did not fire, the ranker mode and rating, the source, when it was added and what is not known (listed in … Arguments: - `item_id` (string, required): the item id - `topic_id` (string): explain the match in this topic (default: the topic that shows the item best) - `lang` (string): language of the labels (en, uk); default is the user's summary language Call: `{"name":"why_shown","arguments":{"item_id":""}}` Result: `{"item_id": "string", "title": "string", "topic_id": "string", "topic_title": "string", "mode": "string", "match_mode": "string", "score": 0, "reason": "string", "signal": 0, "signal_parts": [{…}], "threshold": 0, "passed": false, "matched_words": ["string"], "exclusion_words_not_fired": ["string"], "must_see": "string", "ranker": "string", "ranker_rating": 0.0, "vote_adjust": 0, "quality_flags": ["string"], …` ### Domain: marks #### `complain_check_step` *Complain about a step of a check* (write key) Object to one step of the check an item_check_log shows: a search (kind query), a found page or its class (source), a matching cell (match), a verdict (verdict) or a whole stage (step); reason is wrong_source, missed_source, wrong_class, wrong_verdict or other, with an optional link to a better source and a short note. What it does: writes one complaint row for this user, this step and its claim. Who sees it: the owner, in admin, counted by reason; nobody else. Arguments: - `item_id` (string, required): the item whose check is complained about - `topic_id` (string): the topic the item was seen in (optional) - `kind` (string, required): what the complaint is about: step (ref: the step key), query (ref: the query n), source (ref: the page url), match (ref: the … - `ref` (string, required): the ref of that step as item_check_log gives it - `reason` (string, required): wrong_source, missed_source, wrong_class, wrong_verdict or other - `link` (string): an http(s) address of a better source (optional) - `note` (string): a few words of explanation, up to 400 characters (optional) Call: `{"name":"complain_check_step","arguments":{"item_id":"","kind":"","reason":"","ref":""}}` Result: `{"item_id": "string", "kind": "string", "ref": "string", "reason": "string", "recorded": false, "note": "string"}` #### `list_notes` *Items with a note* (read key) The items the user wrote a note on, most recent note first (at most 200), with their note in mine.note. Arguments: - `lang` (string): language of summaries and translated titles (en, uk); default is the user's summary language Call: `{"name":"list_notes","arguments":{}}` Result: `{"items": [{…}]}` #### `list_saved` *Saved items* (read key) The user's saved (bookmarked) items, most recently saved first (at most 200), as item payloads with mine. Arguments: - `lang` (string): language of summaries and translated titles (en, uk); default is the user's summary language Call: `{"name":"list_saved","arguments":{}}` Result: `{"items": [{…}]}` #### `list_shares` *Your share links* (read key) The share links this account made, newest first, with how often people opened each (preview bots excluded) and how many joined the waiting list from it. live=false is a link that was switched off. Reading changes nothing. Arguments: - none Call: `{"name":"list_shares","arguments":{}}` Result: `{"shares": [{…}]}` #### `mark_item` *Vote, read, save or hide an item* (write key) Do what the app's buttons do with an item of the user's feed: vote up (More like this) or down (Less like this), mark read, save (bookmark), or "not this" (hide). A vote answers `learned` ({direction: more|less, terms}: the topic's own words found in the item, for the Showing-more-like hint; absent when the item has none). Arguments: - `item_id` (string, required): the item id - `topic_id` (string): the topic the item was seen in (its scoring learns from your votes there); default: the topic where the item scored best - `vote` (string): up, down, or none to take the vote back; voting changes the shown score of the item itself (+10 / -30) and the feed learns by … - `read` (boolean): mark read (true) or unread (false) - `saved` (boolean): save (true) or unsave (false) the item; saved items are listed by list_saved - `not_this` (boolean): true hides the item from the feed and removes any vote; false brings it back Call: `{"name":"mark_item","arguments":{"item_id":"6720f1c2a5b7d9e8f0a1b2c3","saved":true,"vote":"up"}}` Result: `{"item_id": "string", "before": {"vote": "string", "read": false, "saved": false, "not_this": false}, "after": {"vote": "string", "read": false, "saved": false, "not_this": false}, "learned": {"direction": "string", "terms": ["string"]}}` #### `mark_items` *Mark many items read at once* (write key) Mark up to 200 items of the user's feed read (or unread with read=false) in one call, like the app's multi-select. Items that are not in the user's feed are listed in not_found and skipped; the answer says which ids changed and which already had the state. Arguments: - `item_ids` (string[], required): the item ids to mark, at most 200 - `read` (boolean): true marks them read (default), false marks them unread Call: `{"name":"mark_items","arguments":{"item_ids":["6720f1c2a5b7d9e8f0a1b2c3","6720f1c2a5b7d9e8f0a1b2c4"],"read":true}}` Result: `{"changed": ["string"], "unchanged": ["string"], "not_found": ["string"]}` #### `note_effects` *What my notes did* (read key) The user's notes (at most 200, newest first) and what feedql did with each (Story 38.7). feedql reads a new or changed note within about a minute and sorts it: negative_preference ("не треба про X": the item is marked Less like this for this user only, so similar items are lowered), todo (the item is saved), quality_complaint (kept as a labelled example of what the feed got wrong; nothing changes), positive_interest and other (recorded; nothing changes). A note it is not sure about changes nothing. Arguments: - none Call: `{"name":"note_effects","arguments":{}}` Result: `{"notes": [{…}]}` #### `report_item` *Report an item to the owner* (write key) Flag an item of the user's feed for the owner (spam, offensive, wrong_content or other): the App Store safeguard for other people's posts. The item leaves the user's feed at once (like not_this). Reporting again only changes the reason. An item that is not in the user's feed is not_found. Twin of the app's Report. Arguments: - `item_id` (string, required): the item id - `reason` (string, required): spam, offensive, wrong_content or other Call: `{"name":"report_item","arguments":{"item_id":"","reason":""}}` Result: `{"item_id": "string", "reported": false}` #### `revoke_share` *Switch off a share link* (write key) Switch off one of the user's share links, by its code or by the item it was made for. The public page then answers 410 with a plain "switched off" page and shows nothing; the item, the feed and the counts already made are untouched. A code that is not the user's, or an item with no live link, is not_found. Effect: marks the link row switched off (it stays in list_shares as live=false); anyone who opens the address sees the plain page. Arguments: - `code` (string): the link's code (the last part of the address) or - `item_id` (string): the item whose link to switch off Call: `{"name":"revoke_share","arguments":{}}` Result: `{"revoked": false, "now": "string"}` #### `save_note` *Write a note on an item* (write key) Set the user's note on an item (at most 2000 characters); an empty text removes the note. Notes are listed by list_notes and shown as mine.note on the item. Effect: the note is stored for this user only (private; not shown to anyone else); within about a minute feedql reads it and, when it is clearly "don't show me this", marks the item Less like this for this user (similar items are lowered in their feed, why_shown says so), and, when it is clearly a to-do (test it, check it), saves the item; a complaint about … Arguments: - `item_id` (string, required): the item id - `text` (string, required): the note (at most 2000 characters); empty removes the note Call: `{"name":"save_note","arguments":{"item_id":"6720f1c2a5b7d9e8f0a1b2c3","text":"check the release notes before upgrading"}}` Result: `{"item_id": "string", "note": "string"}` #### `set_hide_read` *Hide read items* (write key) Set the "Hide read" preference: when on, items marked read leave the user's feed. Arguments: - `on` (boolean, required): true hides read items from the feed Call: `{"name":"set_hide_read","arguments":{"on":true}}` Result: `{"hide_read": false}` #### `share_item` *Make a link to share an item with someone who has no account* (write key) Make the short link /s/ for one item of the user's feed and return it: give it to a person who has no feedql account. Anyone with the link, signed in or not, sees the item's title, summary, source, date and original link, other reports of the same event, a few titles from the same topic, and a button to join the feedql waiting list; they never see the user's notes, votes, marks or other topics. The page is not indexed by search engines. Arguments: - `item_id` (string, required): the item to share, from the feed - `topic_id` (string): the topic of the item's match when it matched several (optional) Call: `{"name":"share_item","arguments":{"item_id":""}}` Result: `{"url": "string", "code": "string", "created": false, "shows": "string", "undo": "string"}` ### Domain: topics #### `add_topic_links` *Add sites and feeds to a topic* (write key) Add sites, feeds, repositories or subreddits where the topic should look, one address per entry. An app store app can be pinned to a country: @ (1232780281@gb) or @ (org.isoron.uhabits@de) read its reviews in that storefront (default us; an app link works too), and versions:@ follows its release notes (items of kind release, place @, metrics.version). A followed topic reads them at once; a website the service has not seen is learned first. Arguments: - `topic_id` (string, required) - `links` (string[], required): addresses of sites, feeds, repositories or subreddits, one per entry Call: `{"name":"add_topic_links","arguments":{"links":["https://go.dev/blog","r/golang","golang/go"],"topic_id":"6720e0a1b2c3d4e5f6a7b8c9"}}` Result: `{"topic": {"id": "string", "title": "string", "channel_id": "string", "status": "string", "following": false, "paused": false, "raw": false, "funnel": {…}, "pinned": false, "state": "string", "unread": 0, "pulse_7d": [0], "spend": {…}, "sites_only": false, "threshold": 0, "window": "string", "geo": ["string"], "languages": ["string"], "pack_version": 0, "about": "string", "filter": "string", "view": false, …` #### `alert_events` *Alerts that were sent* (read key) The alerts your rules sent, newest first: rule, item, bucket, severity, title, source url, whether it updated an earlier alert about the same event, and the sinks that got it. Reading changes nothing. Arguments: - `rule` (string): only this rule's alerts; omit for all of yours - `limit` (integer): alerts to return, 1 to 200 (default 50) Call: `{"name":"alert_events","arguments":{}}` Result: `{"alerts": [{…}]}` #### `create_alert_rule` *Alert me about a topic* (write key) Make an alert rule on a topic (topic_id) or on a non-main channel (channel_id). Whenever a new story that the topic's own filter and the rule's filter (FQL, optional) both accept lands in the safety or roads bucket at severity min_severity (1-3, default 2) or more and is at most a day old, feedql tells you once per event (dedupe_by event, or item); if the same event gets a worse report it tells you again, at most every 6 hours (escalate, default true). Arguments: - `name` (string) - `topic_id` (string) - `channel_id` (string) - `filter` (string) - `min_severity` (integer) - `dedupe_by` (string) - `escalate` (boolean) - `daily_cap` (integer) - `sinks` (string[]) - `enabled` (boolean) Call: `{"name":"create_alert_rule","arguments":{}}` Result: `{"id": "string", "name": "string", "topic_id": "string", "channel_id": "string", "filter": "string", "min_severity": 0, "dedupe_by": "string", "escalate": false, "daily_cap": 0, "sinks": ["string"], "enabled": false, "unreadable": {"at": "string", "reason": "string"}, "created_at": "string", "updated_at": "string", "words": "string", "sent_24h": 0, "overflow": [{…}]}` #### `create_topic` *Create a topic* (write key) Create a topic. With links and no title (or sites_only true) it is made of those sites alone: named after their hosts, no keywords, the feed shows everything the sites publish, AI scoring off (turn it on with update_topic raw false); poll nothing, it is ready at once. Otherwise without keyword_pack the model builds one (poll get_topic until status is ready, uses the daily AI budget); with a pack it is ready at once. Arguments: - `channel` (string): the channel to create the topic in (id or slug from list_channels); empty: the main channel, the reading feed - `title` (string): what to follow, 1-120 characters; may be left out when links are given - `keyword_pack` (object): words, phrases and excluded words; omit to let the model build one - `links` (string[]): links to follow (repositories, feeds, subreddits, pages) - `raw` (boolean): turn AI scoring off for this topic - `sites_only` (boolean): make the topic from links alone: no keywords, everything the sites publish is the feed - `geo` (string[]): limit to places: ISO country codes (UA, DE), EU, EUROPE, NAMER, LATAM, APAC, MENA, REMOTE - `languages` (string[]): limit to items written in these languages (ISO 639-1: en, uk, de) - `excluded` (string[]): exclusion words: items whose title or text has one never reach the feed; whole words or phrases, case-insensitive, a trailing * … - `dont_show` (string[]): what the topic must not show, as short descriptions in any language ("road accidents", "crime", "obituaries"): items about it are … - `must_see` (string[]): words or names whose items ALWAYS show ("Ivan Petrov"): they beat exclusion words, dont_show, turned-off places, vote-similarity … - `markers` (string[]): raw phrases (case-insensitive substrings of an item title or text, at most 50 of 120 characters): the topic matches ONLY items … - `about` (string): what you want from this topic, in your own words, at most 4000 characters, as specific as you like ("senior Go: runtime, … - `match` (string): how keyword_pack words, phrases and markers pre-select items before any model call: words (default; the item contains one as a … - `format` (string): how the summary of an item read through this topic is written, at most 4000 characters, as an instruction ("for an issue: what … - `filter` (string): an FQL filter saved on the topic, at most 500 characters, for example near:"Kandy" 30km during:2026-11-14..2026-11-20 … - `view` (boolean): true: a window on the channel instead of a topic of its own (needs channel, an extra one): it follows no source, has no keywords, … - `exclude_mode` (string): drop (default): items kept out by excluded / dont_show are forgotten; hide: they are kept as filtered rows (never scored by the … Call: `{"name":"create_topic","arguments":{"about":"Senior Go: runtime, performance, concurrency, releases. Not beginner tutorials.","languages":["en"],"title":"golang"}}` Result: `{"topic": {"id": "string", "title": "string", "channel_id": "string", "status": "string", "following": false, "paused": false, "raw": false, "funnel": {…}, "pinned": false, "state": "string", "unread": 0, "pulse_7d": [0], "spend": {…}, "sites_only": false, "threshold": 0, "window": "string", "geo": ["string"], "languages": ["string"], "pack_version": 0, "about": "string", "filter": "string", "view": false, …` #### `delete_alert_rule` *Delete an alert rule* (write key) Delete an alert rule and the alerts it sent. Effect: no more alerts from it; the topic, its items and your webhooks are untouched. This cannot be undone; make a new rule instead (pausing with update_alert_rule keeps the history). Arguments: - `rule` (string, required): the alert rule's id (ar_...) Call: `{"name":"delete_alert_rule","arguments":{"rule":""}}` Result: `{"deleted": false}` #### `delete_topic` *Delete a topic* (write key) Delete a topic with its matches, runs and marks. This cannot be undone. Arguments: - `topic_id` (string, required): the topic id from list_topics Call: `{"name":"delete_topic","arguments":{"topic_id":""}}` Result: `{"deleted": false}` #### `follow_topic` *Follow a topic* (write key) Follow a topic on the sources of a finished test run: pass sources (types) or all. After this its feed fills continuously. Arguments: - `topic_id` (string, required) - `run` (integer): the test run number; defaults to the latest - `sources` (string[]): source types to follow (see list_source_types) - `all` (boolean): follow every source type of the run Call: `{"name":"follow_topic","arguments":{"sources":["reddit","hackernews"],"topic_id":"6720e0a1b2c3d4e5f6a7b8c9"}}` Result: `{"id": "string", "title": "string", "channel_id": "string", "status": "string", "following": false, "paused": false, "raw": false, "funnel": {"day": {…}, "week": {…}}, "pinned": false, "state": "string", "unread": 0, "pulse_7d": [0], "spend": {"month_usd": 0.0, "last_30_days_usd": 0.0, "all_time_usd": 0.0, "by_kind": [{…}], "estimated": false, "attributed_since": "string"}, "sites_only": false, "threshold": 0, …` #### `get_test_run` *Poll a test run* (read key) Poll a test run: status is running until every source is done, then done. Pass run_id, or topic_id and n. Arguments: - `topic_id` (string): with n, instead of run_id - `n` (integer): the run number; the id returned by start_test_run is topic_id:n - `run_id` (string): the run id returned by start_test_run (topic_id:n) Call: `{"name":"get_test_run","arguments":{"n":1,"topic_id":"6720e0a1b2c3d4e5f6a7b8c9"}}` Result: `{"id": "string", "topic_id": "string", "n": 0, "status": "string", "pack_version": 0, "started_at": "string", "finished_at": "string", "total_items": 0, "sources": [{…}]}` #### `get_topic` *One topic* (read key) One topic with its keyword pack, excluded words (excluded; excluded_suggestions are proposals not yet accepted, among them words that keep showing up in items the user marked Less like this; source_suggestions are places (a subreddit, account, site, repository) at least three of those items came from, muted_sources the places already turned off), dont_show (what is kept out by meaning) and must_see (what always shows), pasted links and (when followed) its sources and its funnel {day, week}: collected, filtered out … Arguments: - `topic_id` (string, required): the topic id from list_topics Call: `{"name":"get_topic","arguments":{"topic_id":"6720e0a1b2c3d4e5f6a7b8c9"}}` Result: `{"id": "string", "title": "string", "channel_id": "string", "status": "string", "following": false, "paused": false, "raw": false, "funnel": {"day": {…}, "week": {…}}, "pinned": false, "state": "string", "unread": 0, "pulse_7d": [0], "spend": {"month_usd": 0.0, "last_30_days_usd": 0.0, "all_time_usd": 0.0, "by_kind": [{…}], "estimated": false, "attributed_since": "string"}, "sites_only": false, "threshold": 0, …` #### `get_topic_discovery` *Read a topic's recurring discovery* (read key) Whether the topic keeps looking for sources in the places its filter names, the interests it uses (yours, or the default set when you gave none), the places, every interest x place pair with its runs, the sources it added and why (sources_added, the query or venue), the last run's note, when the next run is, and the day's caps (pairs a day, model cost a day and what was used). A pair that added nothing twice is stopped (stopped true) until rearm_topic_discovery. Arguments: - `topic` (string, required): the topic id (list_topics) Call: `{"name":"get_topic_discovery","arguments":{"topic":""}}` Result: `{"topic_id": "string", "channel_id": "string", "enabled": false, "interests": ["string"], "pairs": [{…}], "added": [{…}], "next_run_at": "string", "last_run_at": "string", "last_note": "string", "places": ["string"], "caps": {"pairs_per_day": 0, "pairs_today": 0, "cost_cap_usd_per_day": 0.0, "cost_today_usd": 0.0, "stop_pair_after_empty_runs": 0}, "uses_default_interests": false, …` #### `list_alert_rules` *Your alert rules* (read key) Your alert rules, each with a sentence saying what it does (words) and how many alerts it sent in the last 24 hours. Reading changes nothing. Arguments: - `topic_id` (string): only the rules of this topic - `channel_id` (string): only the rules of this channel Call: `{"name":"list_alert_rules","arguments":{}}` Result: `{"rules": [{…}]}` #### `list_source_types` *Source types* (read key) The source types a topic can follow (the names used by follow_topic and update_topic). jobs_only types carry vacancies and are offered only to topics that follow vacancies. Arguments: - none Call: `{"name":"list_source_types","arguments":{}}` Result: `{"sources": [{…}]}` #### `list_topics` *List topics* (read key) List the user's topics (id, channel_id, title, status, whether it is followed) of one channel (channel_id is that channel's id, `main` for the reading feed): the main channel (the reading feed) by default, or channel= from list_channels. A followed topic carries funnel {day, week}: for the last 24 h and 7 days how many items were collected, filtered out and why (lang, excluded_words, topic_words, muted_sources, vote_similar, dont_show, far_by_meaning, below_threshold), shown, must_see, and read, … Arguments: - `channel` (string): the channel id or slug from list_channels; empty: the main channel (your reading feed) Call: `{"name":"list_topics","arguments":{}}` Result: `{"topics": [{…}]}` #### `pin_topic` *Pin a topic* (write key) Pin or unpin a topic in the topic switcher (at most 8 pinned). Arguments: - `topic_id` (string, required) - `pinned` (boolean, required): true pins the topic in the topic switcher (at most 8), false unpins Call: `{"name":"pin_topic","arguments":{"pinned":true,"topic_id":""}}` Result: `{"topic_id": "string", "pinned": false}` #### `rearm_topic_discovery` *Let stopped discovery pairs try again* (write key) Clear the stop of the interest x place pairs that added nothing twice (all of them, or one interest's), so the next run tries them again. What happens: they run at the next sweep (within 5 minutes) inside the day's caps; they are stopped again after two more empty runs. Undo: nothing to undo; the pairs simply stop again if they find nothing. Arguments: - `topic` (string, required): the topic id - `interest` (string): only this interest's stopped pairs; omit for all of them Call: `{"name":"rearm_topic_discovery","arguments":{"topic":""}}` Result: `{"topic_id": "string", "channel_id": "string", "enabled": false, "interests": ["string"], "pairs": [{…}], "added": [{…}], "next_run_at": "string", "last_run_at": "string", "last_note": "string", "places": ["string"], "caps": {"pairs_per_day": 0, "pairs_today": 0, "cost_cap_usd_per_day": 0.0, "cost_today_usd": 0.0, "stop_pair_after_empty_runs": 0}, "uses_default_interests": false, …` #### `remove_topic_link` *Remove a site or feed from a topic* (write key) Remove one link (the key from get_topic links[].key) from a topic and stop reading it. A followed topic keeps at least one source. Returns the topic with topic.links[] and their state. Arguments: - `topic_id` (string, required) - `key` (string, required): the link key from get_topic links[].key Call: `{"name":"remove_topic_link","arguments":{"key":"","topic_id":""}}` Result: `{"topic": {"id": "string", "title": "string", "channel_id": "string", "status": "string", "following": false, "paused": false, "raw": false, "funnel": {…}, "pinned": false, "state": "string", "unread": 0, "pulse_7d": [0], "spend": {…}, "sites_only": false, "threshold": 0, "window": "string", "geo": ["string"], "languages": ["string"], "pack_version": 0, "about": "string", "filter": "string", "view": false, …` #### `retry_topic` *Build the keyword pack again* (write key) Ask the model for the keyword pack again after it failed (status failed). A topic in any other state is returned unchanged. Poll get_topic until status is ready. Arguments: - `topic_id` (string, required): the topic id from list_topics Call: `{"name":"retry_topic","arguments":{"topic_id":""}}` Result: `{"id": "string", "title": "string", "channel_id": "string", "status": "string", "following": false, "paused": false, "raw": false, "funnel": {"day": {…}, "week": {…}}, "pinned": false, "state": "string", "unread": 0, "pulse_7d": [0], "spend": {"month_usd": 0.0, "last_30_days_usd": 0.0, "all_time_usd": 0.0, "by_kind": [{…}], "estimated": false, "attributed_since": "string"}, "sites_only": false, "threshold": 0, …` #### `set_topic_discovery` *Turn recurring discovery on or off for a topic* (write key) Make a topic with a place in its filter (near:"Kandy", along:, country:, region:) look for sources by itself, for the interests you give (or live music, festival, market, exhibition). What happens: within 5 minutes and then once a day, for up to 4 interest x town pairs a day, feedql searches the web for " " and lists the venues OpenStreetMap has near the town, reads each candidate with the normal quality gate (dead, empty, undated, spam and paywalled sites are dropped) and adds the survivors to … Arguments: - `topic` (string, required): the topic id; its filter must name a place (near:, along:, country: or region:) - `enabled` (boolean, required): true: look for sources now and every day; false: stop (sources already added stay) - `interests` (string[]): what you want to find there, in your words, at most 20 of 60 characters: jazz, open mic, night market, temple festival. Call: `{"name":"set_topic_discovery","arguments":{"enabled":true,"topic":""}}` Result: `{"topic_id": "string", "channel_id": "string", "enabled": false, "interests": ["string"], "pairs": [{…}], "added": [{…}], "next_run_at": "string", "last_run_at": "string", "last_note": "string", "places": ["string"], "caps": {"pairs_per_day": 0, "pairs_today": 0, "cost_cap_usd_per_day": 0.0, "cost_today_usd": 0.0, "stop_pair_after_empty_runs": 0}, "uses_default_interests": false, …` #### `start_test_run` *Start a test run* (write key) Start a test run of a ready topic across the sources (scores found items with the model within the daily AI budget). Pass sources (types from list_source_types) to test only those; a type the topic is not offered is refused. Poll get_test_run with the returned id. Arguments: - `topic_id` (string, required) - `sources` (string[]): limit the run to these source types (see list_source_types); default all offered to the topic Call: `{"name":"start_test_run","arguments":{"topic_id":"6720e0a1b2c3d4e5f6a7b8c9"}}` Result: `{"id": "string", "topic_id": "string", "n": 0, "status": "string", "pack_version": 0, "started_at": "string", "finished_at": "string", "total_items": 0, "sources": [{…}]}` #### `unfollow_topic` *Unfollow a topic* (write key) Stop following a topic (it stays as a draft with its keyword pack). Arguments: - `topic_id` (string, required): the topic id from list_topics Call: `{"name":"unfollow_topic","arguments":{"topic_id":""}}` Result: `{"id": "string", "title": "string", "channel_id": "string", "status": "string", "following": false, "paused": false, "raw": false, "funnel": {"day": {…}, "week": {…}}, "pinned": false, "state": "string", "unread": 0, "pulse_7d": [0], "spend": {"month_usd": 0.0, "last_30_days_usd": 0.0, "all_time_usd": 0.0, "by_kind": [{…}], "estimated": false, "attributed_since": "string"}, "sites_only": false, "threshold": 0, …` #### `update_alert_rule` *Change or pause an alert rule* (write key) Change a rule's filter, min_severity, dedupe_by, escalate, daily_cap, sinks or name, or pause and resume it with enabled. Effect: the next sweep (within minutes) uses the new settings; alerts already sent stay. The topic or channel of a rule cannot change: make another rule. Undo: change it back. Arguments: - `rule` (string, required): the alert rule's id (ar_...) - `name` (string) - `filter` (string) - `min_severity` (integer) - `dedupe_by` (string) - `escalate` (boolean) - `daily_cap` (integer) - `sinks` (string[]) - `enabled` (boolean) Call: `{"name":"update_alert_rule","arguments":{"rule":""}}` Result: `{"id": "string", "name": "string", "topic_id": "string", "channel_id": "string", "filter": "string", "min_severity": 0, "dedupe_by": "string", "escalate": false, "daily_cap": 0, "sinks": ["string"], "enabled": false, "unreadable": {"at": "string", "reason": "string"}, "created_at": "string", "updated_at": "string", "words": "string", "sent_24h": 0, "overflow": [{…}]}` #### `update_topic` *Edit a topic* (write key) Change a topic: title, keyword_pack (with pack_version from get_topic; a stale one is a conflict), excluded (replaces the exclusion words, [] clears them; whole words or phrases, case-insensitive, a trailing * is a prefix; on a followed topic the last 7 days are re-judged and matching items leave the feed), mute_sources / unmute_sources (turn a place from get_topic source_suggestions[].key off for this topic, or back on: its items stop matching and the ones in the feed are removed), match (words, phrase, parts or … Arguments: - `topic_id` (string, required) - `changes` (object, required): the fields to change; only present ones apply Call: `{"name":"update_topic","arguments":{"changes":{"excluded":["hiring","crypto*"],"threshold":60},"topic_id":"6720e0a1b2c3d4e5f6a7b8c9"}}` Result: `{"topic": {"id": "string", "title": "string", "channel_id": "string", "status": "string", "following": false, "paused": false, "raw": false, "funnel": {…}, "pinned": false, "state": "string", "unread": 0, "pulse_7d": [0], "spend": {…}, "sites_only": false, "threshold": 0, "window": "string", "geo": ["string"], "languages": ["string"], "pack_version": 0, "about": "string", "filter": "string", "view": false, …` ### Domain: data #### `get_export` *Export status and download* (read key) One export or stats job: status (queued, running, ready, failed), rows, size and, when ready, a fresh signed download_url (24 hours) or, for a stats job, its result. Without an id it lists the recent exports. Arguments: - `id` (string): the export id from start_export or item_stats; empty lists the recent exports Call: `{"name":"get_export","arguments":{"id":"6720f1c2a5b7d9e8f0a1b2c5"}}` Result: `…` #### `item_stats` *Count items by day, topic, source…* (read key) Count the items an FQL query finds, grouped by day, week, topic, source, lang, score_bucket or author_count, with the average score and the distinct authors (authors) per bucket; author_count groups by topic and ranks by distinct authors. Counts what the search shows the reader (one per item). A typical range answers at once; a range too big for that answers {status: queued, job_id}: read the result with get_export. Arguments: - `query` (string): an FQL query; empty counts the whole feed - `by` (string, required): day, week, topic, source, lang, score_bucket or author_count - `include` (string): filtered: also return the items a topic in hide exclusion mode kept out (filtered_by says why: exclusion: or … Call: `{"name":"item_stats","arguments":{"by":"day","query":"topic:golang since:30d"}}` Result: `{"by": "string", "buckets": [{…}], "total": 0, "scanned": 0, "capped": false, "status": "string", "job_id": "string"}` #### `start_export` *Export your data* (write key) Start an export of every item matching an FQL query (empty: the whole feed) as a gzip file of JSON lines or CSV: title, url, source, author hash, published, score, rationale, topic, language, event and its reports, feedql's summary in the user's language, the excerpt and the user's vote, save and note; never the full article text. The answer is the export with status queued: poll get_export until ready, then fetch its download_url (signed, valid 24 hours; files are deleted after 7 days). Arguments: - `query` (string): an FQL query: every matching item is exported; empty exports the whole feed (no ~meaning terms) - `format` (string): jsonl (default, one JSON item per line) or csv - `include` (string): filtered: also return the items a topic in hide exclusion mode kept out (filtered_by says why: exclusion: or … Call: `{"name":"start_export","arguments":{"format":"jsonl","query":"topic:golang since:30d"}}` Result: `{"id": "string", "channel": "string", "kind": "string", "status": "string", "q": "string", "format": "string", "include": "string", "by": "string", "rows": 0, "bytes": 0, "truncated": false, "error": "string", "created_at": "string", "finished_at": "string", "expires_at": "string", "filename": "string", "download_url": "string", "result": {"by": "string", "buckets": [{…}], "total": 0, "scanned": 0, …` ### Domain: webhooks #### `create_inbound_webhook` *Create an inbound webhook* (write key) Make an address that accepts pushed items into one of the user's topics. POST /in/ takes one item or {"items":[...]} (up to 100) with title, url, text, published, author, source, tags, fields{}, metrics{}, id; authenticate with Authorization: Bearer or an HMAC signature (see llms-full.txt). The answer carries the secret key once; it cannot be read again. At most 20 per account. Items become source in: and are private to the user. Arguments: - `name` (string, required): 1 to 40 characters; items get the source in: - `topic_id` (string): the topic that receives the items (main feed or a channel). Omit it to get a new raw, already followed topic named Inbox: , … - `mapping` (object): optional: item field (title, url, text, published, author, source, tags, id) to a dotted path in the pushed object, for example … Call: `{"name":"create_inbound_webhook","arguments":{"name":""}}` Result: `{"webhook": {"id": "string", "name": "string", "address": "string", "source": "string", "topic_id": "string", "channel_id": "string", "mapping": {"": "string"}, "enabled": false, "key_hint": "string", "created_at": "string", "key_rotated_at": "string", "last_request_at": "string", "requests": 0, "failed_requests": 0, "items_created": 0, "items_duplicate": 0, "items_rejected": 0}, "key": "string", …` #### `create_webhook` *Create a webhook* (write key) Push a topic's new items (or, with channel, every new item of a channel) to a public https URL, signed with X-Feedql-Signature (HMAC-SHA256 of the body). The answer carries the signing secret once; it cannot be read again. At most 3 per topic or channel and 10 per account. Delivery starts from now. Arguments: - `topic_id` (string): the topic whose new items are pushed (or channel instead) - `channel` (string): the channel (id or slug) whose new items are all pushed, instead of one topic - `url` (string, required): a public https URL that accepts POST Call: `{"name":"create_webhook","arguments":{"topic_id":"6720e0a1b2c3d4e5f6a7b8c9","url":"https://example.com/feedql-hook"}}` Result: `{"webhook": {"id": "string", "topic_id": "string", "channel_id": "string", "items_today": 0, "url": "string", "status": "string", "created_at": "string", "delivered_items": 0, "last_delivered_at": "string", "failing_since": "string", "attempts": 0, "last_error": "string", "disabled_at": "string", "disabled_reason": "string"}, "secret": "string"}` #### `delete_inbound_webhook` *Delete an inbound webhook* (write key) Delete the webhook with its key and delivery log; its address answers 401 from then on. Items it already brought stay in the feed. Arguments: - `id` (string, required): the inbound webhook id (the uuid inside its address) Call: `{"name":"delete_inbound_webhook","arguments":{"id":""}}` Result: `{"deleted": false}` #### `delete_webhook` *Delete a webhook* (write key) Delete a webhook; nothing more is sent to its URL. Arguments: - `id` (string, required): the webhook id Call: `{"name":"delete_webhook","arguments":{"id":""}}` Result: `{"deleted": false}` #### `enable_webhook` *Re-enable a webhook* (write key) Turn a disabled webhook back on (after 24 hours of failures it is disabled): clears the failure state and delivers what was missed. Arguments: - `id` (string, required): the webhook id Call: `{"name":"enable_webhook","arguments":{"id":""}}` Result: `{"id": "string", "topic_id": "string", "channel_id": "string", "items_today": 0, "url": "string", "status": "string", "created_at": "string", "delivered_items": 0, "last_delivered_at": "string", "failing_since": "string", "attempts": 0, "last_error": "string", "disabled_at": "string", "disabled_reason": "string"}` #### `get_webhook` *One webhook* (read key) One webhook with its delivery state: items delivered, failing since, attempts, last error, why it was disabled. Arguments: - `id` (string, required): the webhook id Call: `{"name":"get_webhook","arguments":{"id":""}}` Result: `{"id": "string", "topic_id": "string", "channel_id": "string", "items_today": 0, "url": "string", "status": "string", "created_at": "string", "delivered_items": 0, "last_delivered_at": "string", "failing_since": "string", "attempts": 0, "last_error": "string", "disabled_at": "string", "disabled_reason": "string"}` #### `inbound_webhook_deliveries` *Inbound webhook deliveries* (read key) The last requests the webhook received (up to 50, newest first): time, status, items, how many were created, duplicate or rejected, the error and the first 200 characters of the first item. Never more of the payload. Arguments: - `id` (string, required) - `limit` (integer): 1 to 50, default 50 Call: `{"name":"inbound_webhook_deliveries","arguments":{"id":""}}` Result: `{"deliveries": [{…}]}` #### `list_inbound_webhooks` *List inbound webhooks* (read key) The user's inbound webhooks: addresses (https://api.feedql.com/in/) that accept JSON pushed from scripts, Zapier, n8n, CI or monitoring and write it as items into one of the user's topics. Each with its target topic, on/off, key hint (never the key) and counters. Arguments: - none Call: `{"name":"list_inbound_webhooks","arguments":{}}` Result: `{"webhooks": [{…}]}` #### `list_webhooks` *List webhooks* (read key) The user's webhooks (push of new items to a URL), all or one topic's, with delivery state and the last error. Arguments: - `topic_id` (string): only this topic's webhooks - `channel` (string): only the webhooks attached to this channel (id or slug) Call: `{"name":"list_webhooks","arguments":{}}` Result: `{"webhooks": [{…}]}` #### `rotate_inbound_webhook_key` *Rotate an inbound webhook's key* (write key) Give the webhook a new secret key; the old one stops working at once. The new key is in the answer once. Arguments: - `id` (string, required): the inbound webhook id (the uuid inside its address) Call: `{"name":"rotate_inbound_webhook_key","arguments":{"id":""}}` Result: `{"webhook": {"id": "string", "name": "string", "address": "string", "source": "string", "topic_id": "string", "channel_id": "string", "mapping": {"": "string"}, "enabled": false, "key_hint": "string", "created_at": "string", "key_rotated_at": "string", "last_request_at": "string", "requests": 0, "failed_requests": 0, "items_created": 0, "items_duplicate": 0, "items_rejected": 0}, "key": "string"}` #### `update_inbound_webhook` *Change an inbound webhook* (write key) Rename an inbound webhook, move it to another topic, change its field mapping or switch it on or off. The address and the key stay. Arguments: - `id` (string, required) - `name` (string) - `topic_id` (string): move the webhook to another topic - `mapping` (object): replaces the mapping; {} clears it - `enabled` (boolean): false: the address answers 404 to the right key until it is switched on again Call: `{"name":"update_inbound_webhook","arguments":{"id":""}}` Result: `{"id": "string", "name": "string", "address": "string", "source": "string", "topic_id": "string", "channel_id": "string", "mapping": {"": "string"}, "enabled": false, "key_hint": "string", "created_at": "string", "key_rotated_at": "string", "last_request_at": "string", "requests": 0, "failed_requests": 0, "items_created": 0, "items_duplicate": 0, "items_rejected": 0}` ### Domain: account #### `alert_push_devices` *Browsers that get your alerts* (read key) The browsers that apps such as Wayfeed subscribed to your alerts (web push): which app and when. A browser subscribes itself from the app's page, so there is no tool for that; remove_alert_push_devices switches them off. Addresses and keys are never shown. Arguments: - none Call: `{"name":"alert_push_devices","arguments":{}}` Result: `{"devices": [{…}]}` #### `create_api_key` *Create an API key* (write key) Make an API key for the user (fq_…, for scripts and other agents). Needs name; scope is read (default) or write and never above the access of the connection making the call; expires_in_days is 1 to 365 (default 30) - a key that never expires cannot be made here. The answer carries the full secret ONCE (key): it cannot be read again, so give it to the user or store it right away. At most 10 live keys per user and 5 keys created through MCP an hour. The key is marked as made by MCP in Settings. Arguments: - `name` (string, required): what the key is for, 1 to 60 characters - `scope` (string): read (default) or write; never above the access of the connection making the call - `expires_in_days` (integer): days until the key stops working: 1 to 365, default 30; a key that never expires cannot be made here Call: `{"name":"create_api_key","arguments":{"name":""}}` Result: `{"id": "string", "name": "string", "scope": "string", "key": "string", "display": "string", "expires_at": "string", "origin": "string"}` #### `ext_site_set` *Switch a browser-extension site on or off* (write key) Tell feedql whether the browser extension should read a site for the user (the extension popup's toggle; the extension itself still needs the browser permission, which only the user can grant). Sites are the keys of ext_status. Arguments: - `site` (string, required): the site tag from ext_status, for example linkedin - `enabled` (boolean, required): true: the extension reads this site for the user; false: it stops Call: `{"name":"ext_site_set","arguments":{"enabled":true,"site":""}}` Result: `{"site": "string", "enabled": false, "note": "string"}` #### `ext_status` *Browser extension status* (read key) What the feedql browser extension did for the user: per offered site (x, linkedin, and any site added as a recipe) whether it is switched on, last_synced_at (the last successful update, the time the web app shows as "last update"), the last attempt, whether it succeeded, how many posts it found, any back-off, the recipe version the extension reads the site with, and totals of posts pushed, accepted and already known, plus today's allowance. Arguments: - none Call: `{"name":"ext_status","arguments":{}}` Result: `{"sites": {"": {…}}, "daily": {"used": 0, "cap": 0}, "transcripts": {"used_today": 0, "daily_cap": 0, "summary_used_usd": 0.0, "summary_daily_cap_usd": 0.0}}` #### `get_account` *My account, limits and spend* (read key) The user's email, interface and summary languages, limits (topics, sources per topic, daily AI budget) and what is used today. Arguments: - none Call: `{"name":"get_account","arguments":{}}` Result: `{"id": "string", "settings": {"hide_read": false, "show_images": false, "auto_read": false, "collapse_threads": false, "ungrouped_threads": [{…}], "reputation_ranking": false, "translate_mode": "string"}, "counts": {"topics": 0, "saved": 0, "notes": 0}, "flags": {"push_available": false, "assistant_enabled": false}, "plan": {"id": "string", "paid_until": "string"}, "assistant_daily": 0, "assistant_turns_left": 0, …` #### `list_api_keys` *My API keys* (read key) The user's API keys: name, scope (read or write), the start of the key, last use. Also whether it expires (expires_at), has expired, and where it was made (origin mcp: by an agent). Secrets are never shown. Make a key with create_api_key, revoke one with revoke_api_key, or use the app (Settings, API keys); at most 10 live keys. Arguments: - none Call: `{"name":"list_api_keys","arguments":{}}` Result: `{"api_keys": [{…}]}` #### `list_connected_apps` *Apps connected to the account* (read key) The apps that were let in with OAuth: apps that sign in with feedql (kind app: name, homepage, scopes, the channels it created) and MCP clients such as Claude (kind mcp). Revoked connections are listed too. Disconnect one with revoke_app. Arguments: - none Call: `{"name":"list_connected_apps","arguments":{}}` Result: `{"apps": [{…}]}` #### `list_my_feedback` *Your feedback to the team* (read key) The feedback this account sent with send_feedback, newest first, each with its triage status: received (not looked at yet), triaged, done or wontfix. Only your own messages are listed; the team's internal notes are not shown. Reading changes nothing. Arguments: - `status` (string): only this status: received, triaged, done or wontfix - `limit` (integer): 1 to 200 (default 50) Call: `{"name":"list_my_feedback","arguments":{}}` Result: `{"feedback": [{…}]}` #### `remove_alert_push_devices` *Stop alerts on apps' browsers* (write key) Switch off the web push that apps subscribed for your alerts: one app's devices (app) or all. Effect: deletes those subscriptions; those browsers get no more alerts; your channels, their alert rules and your own feedql browsers are untouched. Undo: the app has to subscribe the browser again from its page. Arguments: - `app` (string): only this app's devices (its client id, for example wayfeed); omit for every app's Call: `{"name":"remove_alert_push_devices","arguments":{}}` Result: `{"removed": 0}` #### `revoke_api_key` *Revoke an API key* (write key) Revoke one of the user's API keys by id (from list_api_keys); it stops working at once. The key making this call cannot be revoked (conflict): use another key or the app. Revoking a revoked key is fine. Connections (Connect to Claude) are not keys; those are disconnected in the app. Arguments: - `id` (string, required): the key's id from list_api_keys Call: `{"name":"revoke_api_key","arguments":{"id":""}}` Result: `{"id": "string", "revoked": false}` #### `revoke_app` *Disconnect an app* (write key) Disconnect one connected app by id (from list_connected_apps). Effect: the connection is marked revoked and its tokens are deleted, so the app's next call answers 401 at once; the app keeps the data it already read, the channels it created stay yours, and nobody else is affected. Undo: not possible; the app has to be connected again on its consent page. The connection making this call cannot be revoked (conflict). Arguments: - `id` (string, required): the connection id from list_connected_apps Call: `{"name":"revoke_app","arguments":{"id":""}}` Result: `{"id": "string", "revoked": false}` #### `send_feedback` *Send feedback to the feedql team* (write key) Use it when data is missing or wrong, a source parses badly, a tool fails unexpectedly or a capability is missing: one report per problem, ids in context, check list_my_feedback first, tell the person. Tell the feedql team something about the product: kind=bug (something is broken or wrong; add severity low|medium|high|critical), kind=feature (something you want) or kind=usage (how it is going, what works or is confusing). Arguments: - `kind` (string, required): bug, feature or usage - `text` (string, required): what happened or what you want, 1 to 4000 characters - `severity` (string): for a bug only: low, medium, high or critical - `context` (object) Call: `{"name":"send_feedback","arguments":{"kind":"","text":""}}` Result: `{"id": "string", "kind": "string", "status": "string"}` #### `set_languages` *Interface and summary languages* (write key) Change the interface language and/or the language summaries and translated titles are written in (en, uk). Omitted ones stay. Arguments: - `ui_language` (string): interface language (en, uk); omit to keep - `summary_language` (string): language of summaries and translated titles (en, uk); omit to keep Call: `{"name":"set_languages","arguments":{}}` Result: `{"ui_language": "string", "summary_language": "string"}` #### `set_show_images` *Show images in the app* (write key) Turn the thumbnails in the feed and the article pictures in the app on or off for this user (the Settings checkbox "Show images"). It changes only what the app shows: items still carry image and images in the API and MCP. Arguments: - `show` (boolean, required): true shows thumbnails and article pictures in the app, false hides them (saves data) Call: `{"name":"set_show_images","arguments":{"show":true}}` Result: `{"show_images": false}` #### `set_translation_mode` *Translate lazily or ahead* (write key) Choose when the summary of an item written in another language is translated for this user (the Settings choice "Translate"). lazy (the default): titles are translated ahead for what you read, and the summary is translated when the item is opened (the item pane, get_item, the item route); a list with summary=1 shows such an item with summary_status on_open and starts nothing. Arguments: - `mode` (string, required): lazy (the default): titles ahead, a summary in another language is written when you open the item; eager: the summary is also … Call: `{"name":"set_translation_mode","arguments":{"mode":""}}` Result: `{"translate_mode": "string"}` ### Domain: profile #### `get_memory` *What feedql knows about me* (read key) The short note feedql keeps of what the user likes, learned from their votes, saves, opens and hides. Lines the user wrote or changed are marked mine. Private to the user. Arguments: - none Call: `{"name":"get_memory","arguments":{}}` Result: `{"lines": [{…}], "updated_at": "string", "updating": false}` #### `get_soul` *My About me* (read key) The user's own words about who they are and what matters to them (role, city, interests, what to ignore), and their per-topic notes. feedql rates items against it. Private to the user. Arguments: - none Call: `{"name":"get_soul","arguments":{}}` Result: `{"about": "string", "updating": false, "topic_notes": {"": "string"}}` #### `set_memory` *Edit or wipe my memory* (write key) Replace the memory with the given lines (a line added or changed is kept as written; a learned line left out is not said again), or wipe it all with wipe=true. Arguments: - `lines` (string[]): the memory as it should now be, one preference per line (at most 12 lines of 4-200 characters, no links); a line you add or … - `wipe` (boolean): true forgets everything feedql learned (lines are ignored) Call: `{"name":"set_memory","arguments":{}}` Result: `{"lines": [{…}], "updated_at": "string", "updating": false}` #### `set_soul` *Write my About me* (write key) Replace the user's About me text (free text, markdown allowed, at most 2000 characters; empty clears it). feedql reads it in the background and uses it to rate items for the user. Arguments: - `about` (string, required): free text, markdown allowed, at most 2000 characters; empty clears it Call: `{"name":"set_soul","arguments":{"about":""}}` Result: `{"about": "string", "updating": false, "topic_notes": {"": "string"}}` #### `set_topic_matters` *What matters in a topic* (write key) Set or clear (empty text) the note on what matters to the user in one topic (at most 400 characters). It counts for relevance in that topic, most of all in a topic made of sites alone. Arguments: - `topic_id` (string, required) - `text` (string, required): what matters in this topic, at most 400 characters; empty removes it Call: `{"name":"set_topic_matters","arguments":{"text":"","topic_id":""}}` Result: `{"topic_id": "string", "text": "string"}` ### Domain: channels #### `add_channel_sources` *Attach raw sources to a channel* (write key) Attach collection sources straight to a channel: what they publish is the channel's items, unscored, with no keywords, no model and no AI budget. Hundreds at a time: App Store apps @, Google Play apps @[:], versions:@ release notes, stored searches and lists (hackernews:/reddit:search:/github:search:/discourse:/search/…/stackexchange:/…/appstore:search:/googleplay:search:|chart:/listings:/federalregister:icr/alerts:nws|caltrans/n8n:all, forms in the sources … Arguments: - `channel` (string, required): the channel id or slug from list_channels - `sources` (string[], required): up to 500 sources: App Store @ or appstore:@, Google Play @[:] or googleplay:@, … - `where` (string): a filter every item of these sources must pass, e.g. rating<=3 or rating<=3,app_genre=Games (keys: metrics such as rating, fields … - `mode` (string): what an item failing where becomes: hide (default; kept, read with include filtered) or drop (not stored for the channel) - `interval_min` (integer): read each source every this many minutes (1 to 43200); 0 or omitted: the automatic schedule - `inactive` (boolean): add them switched off (listed, not collected until update_channel_source active=true) - `spread` (boolean): spread the first read of each over its own interval instead of reading all at once (use for hundreds) Call: `{"name":"add_channel_sources","arguments":{"channel":"","sources":[""]}}` Result: `{"added": [{…}], "existing": [{…}], "problems": [{…}]}` #### `channel_entity` *One entity of a channel's graph* (read key; also offered to a channel key) The entity view of the web app for one entity=type:value (e.g. tool:stripe, repo:golang/go): how many items name it in the period (items) and all time (total), first_seen and last_seen, its mentions over time (series: buckets points over the period, oldest first, each with the bucket start and the number of items), its neighbours (items naming both, weight, up to three sample item ids) and the most recent item ids that name it (fetch them with get_item). found false: the channel never named it. Arguments: - `channel` (string): the channel id or slug; a channel-key session needs none - `entity` (string, required): type:value, e.g. tool:stripe or repo:owner/name - `since` (string): the period: a date (2026-10-01), an RFC 3339 time or a span like 30d; default all time - `limit` (integer): how many neighbours and recent item ids (default 20, at most 100) - `buckets` (integer): points of the mentions-over-time series (default 13, at most 120) Call: `{"name":"channel_entity","arguments":{"entity":""}}` Result: `{"enabled": false, "found": false, "entity": {"type": "string", "value": "string", "items": 0}, "first_seen": "string", "last_seen": "string", "total": 0, "series": [{…}], "neighbours": [{…}], "recent_items": ["string"]}` #### `channel_follow_status` *Where an import's test runs and follows stand* (read key) The summary of import_channel with follow true: state running or done, and per topic pending, running (pack or test run going), followed or failed with found (items the test run listed; 0 is reported plainly), scanned, the source types followed, a note and the reason a topic was not followed. Poll it every few seconds until state is done; a topic with a pack to build can take a minute, a test run up to three. Arguments: - `channel` (string, required): the channel id or slug from list_channels Call: `{"name":"channel_follow_status","arguments":{"channel":""}}` Result: `{"channel": "string", "state": "string", "topics": [{…}], "followed": 0, "failed": 0, "waiting": 0, "started_at": "string", "finished_at": "string"}` #### `channel_graph` *Entity graph of a channel* (read key; also offered to a channel key) What a channel's items talk about and what is named together. Without entity: the top entities by the number of items that named them (types tool, repo, app, company, domain), optionally one type and since a date or span. With entity=type:value (e.g. tool:stripe): that entity's item count and its neighbours, each with the weight (items naming both) and up to three sample item ids to fetch with get_item. Needs the channel option graph (update_channel payload.graph); with it off the answer has enabled false. Arguments: - `channel` (string): the channel id or slug; a channel-key session needs none - `entity` (string): type:value, e.g. tool:stripe or repo:owner/name; returns its neighbours instead of the top entities - `type` (string): only entities of this type: tool, repo, app, company, domain - `since` (string): only entities seen since: a date (2026-10-01), an RFC 3339 time or a span like 7d or 36h - `limit` (integer): how many to return (default 20, at most 100) Call: `{"name":"channel_graph","arguments":{}}` Result: `{"enabled": false, "entities": [{…}], "entity": {"type": "string", "value": "string", "items": 0}, "neighbours": [{…}]}` #### `channel_items` *Search a channel (FQL)* (read key; also offered to a channel key) Search the items a channel received, newest first, with a query in the feedql query language (empty: everything). Pass the returned next as cursor for older items. (FQL syntax: section 6 of this page.) include=filtered also returns the items a topic in hide exclusion mode kept out (each has filtered_by: exclusion: or filter:, and no score). view=filtered leaves out the raw stream of the channel's own sources (the app's Filtered view); the default view=all returns everything. Arguments: - `channel` (string): the channel id or slug; a session made with a channel key is bound to its channel and needs none - `query` (string): an FQL query; empty lists everything the channel received - `include` (string): filtered: also return the items a topic in hide exclusion mode kept out (filtered_by says why: exclusion: or … - `cursor` (string): the next value of the previous answer, to get older items - `lang` (string): language of the title and summary, an ISO 639-1 code (en, uk, de, fr, es, pl and more; an unknown code answers with the list); … - `summary` (boolean): true: every item carries feedql's summary in lang (one not written yet is started, summary_status says pending; at most 25 start … - `transcript` (string): none (default), text or segments: a YouTube video carries its transcript (the spoken words, from captions) in transcript.text, or … - `transcript_max` (integer): characters of transcript per item, 1 to 100000 (default 20000); only with transcript text or segments - `group` (string): item: list each item once with a topics array (topic id, title, score, rationale, seq) of the topics it matched; default: one row … - `order` (string): order inside each group: severity (most serious, official first, newest), time or score; default the page order - `collapse` (string): event: one entry per event in the groups, the lead report with event_count - `tz` (string): IANA zone the days are cut in, e.g. Asia/Colombo; default UTC - `limit` (integer): rows in the page, 1 to 200; default 30 - `sort` (string): points, comments, engagement or relevance: the most first, each item once; needs since: in the query; truncated: true says the … - `legacy_id` (string): the id the item had in news-cli; returns just that item (imported history); query and cursor are ignored - `view` (string): filtered: only the channel's own topics, without the raw stream of its sources; all (default): everything the channel received Call: `{"name":"channel_items","arguments":{}}` Result: `{"items": [{…}], "next": "string", "status": "string", "retry_after_seconds": 0, "message": "string", "retention_days": 0, "truncated": false, "groups": [{…}], "counts": {"feed": 0, "feed_more": false, "collected": 0, "collected_capped": false, "collected_not_in_feed": 0}, "collected": {"available": false, "reason": "string", "items": [{…}], "next": "string", "ignored": ["string"]}}` #### `channel_keep` *Pin items of a channel* (write key; also offered to a channel key) Pin items (by ids or legacy_ids, up to 5000 a call) so the channel's retention never removes them, like a Save: while pinned they stay however old they are. Answers requested, changed (rows pinned) and unknown (ids that no item has). Arguments: - `channel` (string): the channel id or slug; a channel-key session needs none - `ids` (string[]): item ids to pin or unpin - `legacy_ids` (string[]): ids the items had in news-cli (imported history) Call: `{"name":"channel_keep","arguments":{}}` Result: `{"requested": 0, "changed": 0, "unknown": ["string"]}` #### `channel_markers` *Marker hit statistics of a channel* (read key; also offered to a channel key) How precise each topic marker is (raw phrases set with markers on create_topic / update_topic): per topic and marker the items it hit, how many of those were good (scored at or above the topic threshold, or voted up) and the rate good/hits, over the last 7 and 30 days. Markers without a hit are listed with zeros; a marker that no longer belongs to the topic is not. Counters are written by the pipeline in batches, so a hit appears within a minute of the item arriving. Arguments: - `channel` (string): the channel id or slug; a channel-key session needs none Call: `{"name":"channel_markers","arguments":{}}` Result: `{"topics": [{…}]}` #### `channel_network` *Network of a channel's top entities* (read key; also offered to a channel key) The network view of the web app: the top entities of a period (nodes, each with the number of items that named it) and the co-mentions among them (edges: a and b are indices into nodes, weight is the items naming both), heaviest first. type narrows to one kind, nodes is how many (default 60, at most 300), since a date or span like 30d. total is how many entities the period holds. Needs the channel option graph. Arguments: - `channel` (string): the channel id or slug; a channel-key session needs none - `type` (string): only entities of this type: tool, repo, app, company, domain - `since` (string): the period: a date (2026-10-01), an RFC 3339 time or a span like 30d; default all time - `nodes` (integer): how many top entities (default 60, at most 300) Call: `{"name":"channel_network","arguments":{}}` Result: `{"enabled": false, "total": 0, "nodes": [{…}], "edges": [{…}]}` #### `channel_new_items` *A channel's items since a cursor* (read key; also offered to a channel key) What reached a channel since a cursor, oldest first, for polling programs. Call without a cursor first to get the cursor of now, then pass the returned cursor each time; while more is true call again. Reading changes nothing. include=filtered also returns the items a topic in hide exclusion mode kept out (each has filtered_by: exclusion: or filter:, and no score). view=filtered leaves out the raw stream of the channel's own sources; default all. Arguments: - `channel` (string): the channel id or slug; a channel-key session needs none - `topic_id` (string): only this topic of the channel - `include` (string): filtered: also return the items a topic in hide exclusion mode kept out (filtered_by says why: exclusion: or … - `cursor` (string): the cursor of the previous answer; omit it the first time to get the cursor of now - `lang` (string): language of the title and summary, an ISO 639-1 code (en, uk, de, fr, es, pl and more); the title is the translation or empty … - `summary` (boolean): true: every item carries feedql's summary in lang (one not written yet is started, summary_status says pending; at most 25 start … - `transcript` (string): none (default), text or segments: a YouTube video carries its transcript (the spoken words, from captions) in transcript.text, or … - `transcript_max` (integer): characters of transcript per item, 1 to 100000 (default 20000); only with transcript text or segments - `view` (string): filtered: only the channel's own topics, without the raw stream of its sources; all (default): everything the channel received Call: `{"name":"channel_new_items","arguments":{}}` Result: `{"items": [{…}], "cursor": "string", "more": false, "retention_days": 0}` #### `channel_sources` *A channel's raw sources* (read key) The raw collection sources attached to a channel (no keywords, no model, no AI spend): id, key, type, label, active, interval_min (0: automatic), where and mode (the item filter), when last collected, the last error and when it runs next. Newest first, a page at a time (offset, limit; total says how many match); query, type and status narrow the list. Add with add_channel_sources; the number you may have is the sources-per-topic limit in get_account. Arguments: - `channel` (string, required): the channel id or slug from list_channels - `offset` (integer): skip this many sources (newest first) - `limit` (integer): page size, 1 to 500 (default 200) - `query` (string): only sources whose title or key (handle, link) contains this, case-insensitive - `type` (string): only sources of this type, e.g. reddit, appstore - `status` (string): paused (switched off) or failing (the last read failed) Call: `{"name":"channel_sources","arguments":{"channel":""}}` Result: `{"sources": [{…}], "total": 0, "offset": 0}` #### `channel_sparql` *SPARQL over a channel's graph* (read key; also offered to a channel key) Ask the channel's entity graph a SPARQL 1.1 question: SELECT (rows of variable to value), ASK (boolean) or CONSTRUCT (Turtle). Read-only and scoped to this channel's own graph; at most 1000 rows, 5 seconds. Namespace https://feedql.com/ns# (fq): fq:Item (fq:published xsd:dateTime, fq:fromSource, fq:inTopic, fq:inChannel, fq:mentions) and fq:Entity with the kinds fq:Tool, fq:Repo, fq:App, fq:Company, fq:Domain (fq:entityType 'tool', rdfs:label the name); property paths work (?e a/rdfs:subClassOf* fq:Entity). Arguments: - `channel` (string): the channel id or slug; a channel-key session needs none - `query` (string, required): a SPARQL 1.1 SELECT, ASK or CONSTRUCT over the channel's graph (default graph = this channel; no UPDATE, DESCRIBE, FROM, SERVICE) Call: `{"name":"channel_sparql","arguments":{"query":""}}` Result: `{"type": "string", "vars": ["string"], "rows": [{"": "string"}], "boolean": false, "turtle": "string", "truncated": false}` #### `channel_stats` *Count a channel's items* (read key; also offered to a channel key) Count the items of a channel an FQL query finds, grouped by day, week, topic, source, lang, score_bucket or author_count, with the average score and the distinct authors (authors) per bucket; author_count groups by topic and ranks by distinct authors. A typical range answers at once; a range too big answers {status: queued, job_id}: read it with get_export. include=filtered also returns the items a topic in hide exclusion mode kept out (each has filtered_by: exclusion: or filter:, and no score). Arguments: - `channel` (string): the channel id or slug; a channel-key session needs none - `query` (string): an FQL query; empty counts everything - `include` (string): filtered: also return the items a topic in hide exclusion mode kept out (filtered_by says why: exclusion: or … - `by` (string, required): day, week, topic, source, lang, score_bucket or author_count Call: `{"name":"channel_stats","arguments":{"by":""}}` Result: `{"by": "string", "buckets": [{…}], "total": 0, "scanned": 0, "capped": false, "status": "string", "job_id": "string"}` #### `channel_unkeep` *Unpin items of a channel* (write key; also offered to a channel key) Take the pin off items (same arguments as channel_keep); they expire with the channel's retention again. Arguments: - `channel` (string): the channel id or slug; a channel-key session needs none - `ids` (string[]): item ids to pin or unpin - `legacy_ids` (string[]): ids the items had in news-cli (imported history) Call: `{"name":"channel_unkeep","arguments":{}}` Result: `{"requested": 0, "changed": 0, "unknown": ["string"]}` #### `channel_usage` *A channel's volume and spend* (read key) How a channel did in the last 7 days, sizes only: items per day, matched / scored / waiting / without an AI score, dropped by don't-show filters, the top sources, the sources that fail, spend today and in 7 days against the channel's own daily AI cap, how many items have custom field values, and llm_spend: what the channel and its topics cost in model calls this month, in the last 30 days and all time, by kind of work with tokens. Arguments: - `channel` (string, required): the channel id or slug from list_channels Call: `{"name":"channel_usage","arguments":{"channel":""}}` Result: `{"days": [{…}], "matched": 0, "scored": 0, "waiting": 0, "unscored": 0, "dropped_by_filters": 0, "top_sources": [{…}], "failing_sources": [{…}], "spend_today_micro": 0, "spend_7d_micro": 0, "cap_micro": 0, "fields_filled": 0, "spend_today_usd": 0.0, "spend_7d_usd": 0.0, "cap_usd": 0.0, "llm_spend": {"month_usd": 0.0, "last_30_days_usd": 0.0, "all_time_usd": 0.0, "by_kind": [{…}], "estimated": false, …` #### `collect_channel_source` *Collect a channel source now* (write key) Make one source due at once; the collector starts it within a minute (rate limits of the upstream still apply, a blocked host stays blocked). Read the result in channel_sources: last_collected_at and last_error. A switched-off source is refused. Arguments: - `channel` (string, required): the channel id or slug from list_channels - `source` (string, required): the source id (src_…) from channel_sources Call: `{"name":"collect_channel_source","arguments":{"channel":"","source":""}}` Result: `{"id": "string", "key": "string", "type": "string", "label": "string", "url": "string", "active": false, "interval_min": 0, "interval_fixed": false, "where": "string", "mode": "string", "added_at": "string", "last_collected_at": "string", "last_error": "string", "next_run_at": "string"}` #### `create_channel` *Create a channel* (write key) Create an extra channel (2 by default per user, raised by the admin). Then add its topics with create_topic channel=, follow them, and read the data with channel_items or at https://api.feedql.com/c//items with a channel key. Arguments: - `name` (string, required): 1-60 characters - `slug` (string): lowercase letters, digits and dashes, 1-40 characters; made from the name when left out - `description` (string): what the channel is for, at most 300 characters Call: `{"name":"create_channel","arguments":{"name":""}}` Result: `{"id": "string", "slug": "string", "name": "string", "description": "string", "main": false, "paused": false, "learn_votes": false, "no_public": false, "geo": false, "payload": {"summary": false, "summary_lang": "string", "excerpt": false, "text": false, "body": false, "marks": false, "images": false, "entities": false, "graph": false, "explain": false}, "fields": [{…}], "fields_version": 0, "topics": 0, …` #### `create_channel_from_template` *Make a channel from a built-in template* (write key) Expand a built-in template (list_channel_templates). trip: params {name, stops[{place, lat, lon, radius_km, from, to, country, admin}], lang, alert_sev, lead_days}. It makes a private channel that reads places, one topic per stop (shows only items about places within the stop's radius and, with dates, during them) following the sources found for that place, a lifecycle policy (faster collection around the dates, then archive 7 days, delete 30), translation into lang, and an alert rule for safety and road items. Arguments: - `template` (string, required): the built-in template, from list_channel_templates (trip) - `params` (object, required): the template's parameters; for trip: name, stops, lang, alert_sev, lead_days - `dry_run` (boolean): true: return the plan in words and write nothing - `channel` (string): apply the template to this channel of yours (id or slug) instead of making a new one; stops that have their topic already are … Call: `{"name":"create_channel_from_template","arguments":{"params":{"name":"","stops":[]},"template":"