> ## Documentation Index
> Fetch the complete documentation index at: https://docs.quivr.thevibecompany.co/llms.txt
> Use this file to discover all available pages before exploring further.

# X lists (x_list): operator guide

> Collect posts from an X list

An `x_list` Connector Instance polls the posts of one X list through the X API
v2 and turns each post into a Record. Edited posts become corrections, and
posts that are deleted or made protected within the recheck window are
withdrawn. Read the [overview](/connectors) first for what every kind shares:
creation, credentials, health, disable.

<h2 id="what-you-bring">
  What you bring
</h2>

* **The x-list plugin pinned.** `x_list` comes from the first-party connector
  plugin [`plugins/x-list`](/plugins/x-list). `make dev`, `make verify` and the
  Railway image pin it; elsewhere [pin it](/plugins/run-a-connector-plugin).
  Coming from the former built-in kind: until it is pinned, instances report
  `unsupported_connector_kind` and keep their checkpoint, then resume without
  duplicates. The engine setting `x.api_endpoint` is gone; the pin's
  `configuration.api_endpoint` replaces it, for test fakes only.
* **An X developer account and app.** Create a Project and App in the X
  Developer Console. The account owner pays for API usage; check X's pricing
  page and your console for current rates, deduplication rules and plan caps.
  Quivr makes no cost commitment.
* **An app-only bearer token.** Deposit it as the credential. It must be able
  to read the list. Set `expires_at` if your organization rotates tokens on a
  schedule.
* **The consumer secret (optional).** It is stored encrypted, and needed only
  for [webhook mode](/connectors/x-webhooks), which receives posts in real time.
* **A list id.** This is the numeric id in the list URL
  (`https://x.com/i/lists/<list_id>`). Use a public list, or a list the app's
  token can read. An app-only token cannot read another account's private list.

<h2 id="create-an-instance">
  Create an instance
</h2>

```http theme={null}
POST /v0/connectors
{
  "idempotency_key": "watchlist-1",
  "corpus_id": "corpus_…",
  "source_namespace": "x-watchlist",
  "kind": "x_list",
  "config": {
    "list_id": "1234567890123456789",
    "backfill_since": "<an RFC 3339 instant within the last 7 days>",
    "max_reads_per_day": 5000,
    "recheck_window_seconds": 86400,
    "recheck_interval_seconds": 600
  },
  "schedule": { "interval_seconds": 120 },
  "credential": { "secret": { "bearer_token": "…", "consumer_secret": "…" } }
}
```

| Config field | Default | Meaning |
| - | - | - |
| `list_id` | required | Numeric X list id |
| `backfill_since` | none | Also collect posts created since this instant. It must be within the 7 days before the first poll; otherwise that poll fails with `invalid_config` and nothing is collected. Without it, collection starts at the first poll ("start now") |
| `max_reads_per_day` | none | Daily spend guard in posts read (minimum 100), see [Spend](#interval-and-spend) |
| `recheck_window_seconds` | 86400 (24 h) | How long collected posts are rechecked for deletion and protection (1 h to 7 days) |
| `recheck_interval_seconds` | 600 (10 min) | How often that recheck runs (minimum 60 s; a shorter value fails the first poll with `invalid_config`) |

`schedule.interval_seconds` defaults to 120 s. The deployment floor is 30 s.

<h2 id="what-is-collected">
  What is collected
</h2>

* **Record Key** is the id of the original post. An edited post keeps its
  Record: X gives each edit a new post id, but the connector keys every version
  by the first id in `edit_history_tweet_ids`.
* **Version content** is the post text, or the full text of a long post, as the
  `body` Part.
* **Relations.** Quoted, replied-to and reposted posts become Relations
  (`quotes`, `replies_to`, `reposts`) to Records of the same Source Namespace.
  They resolve only if that post was collected too.
* **The `connector.x_list` extension (v1)** records the post id, edit history,
  author (id, username, name), `created_at`, `lang`, `conversation_id`,
  entities (URLs, mentions, hashtags), referenced posts, media references (key,
  type, URL, preview URL, alt text) and the post URL. Media files are not
  downloaded.
* **Edits.** An edit becomes a new Record Version (a correction). The Source
  Position is the newest version's id, so an older version can never replace a
  newer one.

<h2 id="how-polling-works">
  How polling works
</h2>

The X list endpoint has no `since_id`. Each run therefore reads the list newest
first and stops at the newest post of the previous completed pass (the
watermark) or at the start point. A run reads at most 10 pages of up to 100
posts. A longer backlog continues in the next run from the saved position, and
the watermark only advances once the whole backlog is read. The checkpoint is
saved only after each page is accepted, so a restart never loses or duplicates
posts. X's list endpoint may not reach far back, so a large backfill can be
partial.

<h2 id="deletion-and-protection">
  Deletion and protection
</h2>

X's developer policy requires stored content to be removed once it is deleted
or made protected on X. The connector checks recent posts by id
(`GET /2/tweets?ids=`, 100 per request):

* **Deleted, protected, suspended or otherwise unavailable posts** are
  withdrawn (Tombstone, reason `source_withdrawn`), which removes them from
  search and retrieval.
* **Only posts within `recheck_window_seconds`** of their creation are checked.
  The check runs every `recheck_interval_seconds`. A deletion is reflected after
  at most about the recheck interval plus the polling interval (≈ 12 minutes
  with the defaults).
* **At most 2,000 posts** are tracked. If more arrive within the window, the
  oldest leave the recheck early. `health.diagnostics.recheck_dropped_posts`
  counts them.
* **A withdrawn Record stays withdrawn.** If the author later makes the post
  public again, it is not re-collected; the attempt shows as `item_rejected`.

> **Known limitation: deletions older than the recheck window are not
> detected.** The policy's removal obligation applies whatever the post's age,
> including within 24 hours of a removal request from X or the author. This
> connector does not yet cover posts older than `recheck_window_seconds`. If
> you must honour those requests, remove the Records manually
> (`POST /v0/records/withdrawals`) until long-tail compliance support ships.
> Raising the window to its 7-day maximum narrows the gap but raises read
> costs.

`health.diagnostics` shows the current coverage:

```json theme={null}
{ "recheck_window_seconds": 86400, "recheck_interval_seconds": 600,
  "recheck_tracked_posts": 37, "recheck_dropped_posts": 0,
  "last_recheck_at": "2026-09-28T10:00:00Z" }
```

<h2 id="interval-and-spend">
  Interval and spend
</h2>

X bills reads per post returned, not per request, and deduplicates repeated
reads of the same post within a UTC day (a "soft guarantee"). Roughly, the
posts read per UTC day are:

* **New posts.** Every new post in the list is read once.
* **The top page.** It is re-read on each poll but deduplicated, so it costs at
  most about 100 extra posts per UTC day.
* **Rechecks.** Each post is read at most once more on each UTC day it stays in
  the window. That is about one extra read per post with a 24 h window, and
  more with longer windows.

A shorter interval lowers latency. Because of the per-day deduplication, it
does not multiply cost. Controls:

* **`health.usage`.** `items_read` estimates today's billed post reads (UTC)
  and `previous_day_items_read` yesterday's. It counts each post once per UTC
  day, as X deduplicates: the day's first list page, new posts, and the first
  recheck of a post each day. X's deduplication is a soft guarantee, so compare
  with the console when the numbers matter.
* **`max_reads_per_day`.** Once reached, polling for new posts stops until the
  next UTC day, and `last_error.code` is `daily_read_cap_reached` while health
  stays healthy. Deletion rechecks continue, because compliance comes first. A
  single request can overshoot the cap by up to 100 posts.
* **The spend limit in the X Developer Console.** It is the hard stop; when it
  is reached, X answers 402 (below).

<h2 id="health-codes">
  Health codes
</h2>

| `last_error.code` | Health | Meaning | What to do |
| - | - | - | - |
| `unauthorized` | `access_error` | X refused the token (401) | Deposit a valid bearer token |
| `forbidden` | `access_error` | The app may not read this list (403 or not-authorized) | Check the list is public or readable by the app, and the app's access level |
| `list_not_found` | `access_error` | The list id does not exist (404 or not-found) | Disable the instance and create one with the right `list_id` on the same Source Namespace |
| `credits_depleted` | `access_error` | X answered 402: prepaid credits or the spend limit are exhausted | Add credits or raise the limit in the X Developer Console; collection resumes on the next successful poll |
| `rate_limited` | unchanged | X answered 429 | Nothing; the next run waits for `x-rate-limit-reset` (at most 15 min). Frequent occurrences mean another app shares the limit or the interval is too short |
| `daily_read_cap_reached` | unchanged | `max_reads_per_day` reached | Raise the cap or wait for the next UTC day |
| `source_unavailable` | unchanged | Network error, timeout or 5xx | Nothing, unless it persists |
| `invalid_response` | unchanged | X returned an unreadable body | Report it if it persists |
| `invalid_config` | unchanged | `backfill_since` is more than 7 days before the first poll | Disable the instance and create one with a later `backfill_since` |
| `item_rejected` | unchanged | A post could not be accepted (e.g. a withdrawn post reappeared) | Usually harmless; the rest of the list is still collected |

`access_error` lasts until a later successful poll.

<h2 id="troubleshooting">
  Troubleshooting
</h2>

* **Nothing is collected.**
  * Without `backfill_since`, only posts created after the first poll are
    collected.
  * Check that `last_success_at` advances and that `health.usage.items_read`
    grows.
* **An edited post shows the old text.** The correction appears on the next
  poll after the edit. Check the Record's current Version.
* **A post deleted on X is still searchable.**
  * Check `health.diagnostics.last_recheck_at` and whether the post is older
    than `recheck_window_seconds` (see the known limitation above).
  * If `recheck_dropped_posts` is non-zero, the list produced more than 2,000
    posts within the window.
* **Costs are higher than expected.** Compare `health.usage` with the console,
  lower the recheck window, or set `max_reads_per_day`.
