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

# MCP tools for tweet search, followers & X actions

> Choose API MCP tools for tweet search, profile lookup, follower exports, monitors, webhooks, account actions, pagination, and sandbox work. See tool examples.

<blockquote className="agent-llms-directive">
  For the complete documentation index, see <a href="/llms.txt">llms.txt</a>.
</blockquote>

Xquik API MCP supports 2 explicit tool modes. Code Mode uses `docs`, `search`, and `execute`. Native mode exposes `docs`, `readSavedResult`, and one tool for each eligible OpenAPI operation. Active guest `paid_reads` keys see only the eligible paid-read routes in either mode.

Modern clients negotiate MCP `2026-07-28` through `server/discover`. Current SDKs add required request metadata and headers.
See [MCP 2026-07-28](/mcp/overview#mcp-2026-07-28).

## Native OpenAPI tools

Connect to `https://xquik.com/mcp?codemode=false` for ordinary MCP tools. Full
credentials get `docs`, `readSavedResult`, and 1 tool for each eligible
operation. Guest keys get `docs`, `readSavedResult`, and 1 tool for each
eligible GET route.

Native names, schemas, and contracts come from OpenAPI. `compose` and
`estimateExtraction` are read-only. Clients read each JSON Schema from
`tools/list`. The server embeds no model prompt, routing, sample count, or endpoint preference.

Use `https://xquik.com/mcp` for Code Mode. Choose native mode for one tool per operation. Binary downloads and credential lifecycle operations remain REST-only.

## docs

Search canonical public documentation. `docs` is read-only and uses no credits.

**Input.** Pass a non-empty `query` string of up to 500 characters.

## readSavedResult

Native mode reads a saved oversized answer in pages. Pages make no API requests and use no credits.

**Input.** Pass `result_id` from the oversized answer and optional `offset`, which starts at 0.

**Output.** Each page returns `characters`, `next_offset`, and `text` within 24,000 characters.
Pass `next_offset` as `offset` until it is `null`, then join each `text` in order.
A page never splits a character.

## search

Search the authenticated API catalog. `search` makes no network calls and uses no credits. Full credentials search every catalog route. Guest keys search the eligible paid-read routes.

**Input.**

Pass required `code` as an async arrow function of up to 10,000 characters.
Read `spec.paths`. Return matching paths, methods, and relevant contract fields.
Inspect inputs and response fields separately when an operation is large.

**Sandbox API.**

Inputs resolve inline. Response `$ref` values point into `spec.components.schemas`.
The catalog includes only response schemas reachable from your allowed operations.
Search retains operation-specific response descriptions. Date-only fields keep string types.
Follow references to inspect shared fields and recursive types.

```typescript theme={null}
interface OperationInfo {
  operationId: string;
  summary?: string;
  description?: string;
  tags?: string[];
  parameters?: unknown[];
  requestBody?: unknown;
  responses?: Record<string, unknown>;
}

interface PathItem {
  get?: OperationInfo;
  post?: OperationInfo;
  put?: OperationInfo;
  patch?: OperationInfo;
  delete?: OperationInfo;
}

declare const spec: {
  paths: Record<string, PathItem>;
  components: { schemas: Record<string, unknown> };
};
```

**Examples.**

> Inspect an operation's input contract

```javascript theme={null}
async () => {
  const path = "/api/v1/x/tweets/search";
  const { operationId, summary, parameters } = spec.paths[path].get;
  return { path, method: "GET", operationId, summary, parameters };
};
```

Inspect specific response properties in a separate `search` call.
For example, `#/components/schemas/PaginatedTweets` identifies the shared pagination schema.

```javascript theme={null}
async () => spec.components.schemas.PaginatedTweets.properties;
```

## execute

Execute API calls allowed by the authenticated credential. Full account keys and OAuth tokens keep their existing account capabilities. Guest keys can execute only the eligible GET reads. The server injects authentication and required idempotency headers.

**Input.**

Pass `code` as an async arrow function of up to 10,000 characters. `xquik` is global and the function's first argument.

Pass optional `result_id` to load a saved result into `xquik.result`.
Without it, `xquik.result` is `null`.

Pass the function itself, not a promise or its result.
Place API calls inside its body. Non-functions fail before any API request starts.

`path` works with or without the `/api/v1` prefix, as in `/x/users/nasa/follow`.

**Sandbox API.**

```typescript theme={null}
interface XquikRequest {
  path: string;
  method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
  query?: Record<string, string | number | boolean | undefined>;
  body?: unknown;
}

interface XquikResponse<T> {
  success: true;
  status: number;
  result: T;
  errors: [];
  messages: [];
}

declare const xquik: {
  result: unknown;
  request<T = unknown>(options: XquikRequest): Promise<XquikResponse<T>>;
};
```

**Response contract.**

`xquik.request()` returns `success`, `status`, `result`, `errors`, and `messages`.
`status` contains the HTTP status. `result` contains the endpoint body.
A rejected request includes `error.status` when available. Existing string-path
calls still return the body for compatibility. Hosted MCP injects a unique
`Idempotency-Key` and reuses it for bounded transient retries. Verify unresolved
writes. Retry only when `safe_to_retry` is true. Results use snake\_case with Unix
timestamps. CamelCase reads work, including `favoriteCount` for `like_count`. Errors stay structured. Pagination uses `has_more` and `next_cursor`.

Compact extraction results retain `like_count`, `retweet_count`, `reply_count`, `quote_count`, `view_count`, and `bookmark_count`. They omit profile images and nested enrichment.

MCP keeps every safe field that X supplies. Optional fields stay absent. See [Read Data Richness](/guides/tweet-profile-api-fields) for the REST field map.

Missing response-field reads return `undefined` and produce `warnings`.
Each warning maps a missing field to available response keys.
Optional-field fallbacks remain valid. Warnings contain field names, never response values.
With warnings, text and structured output both contain `{ result, warnings }`.
The combined output follows the existing 24,000-character limit.

List and search responses use `has_more` and `next_cursor`, even when REST shows `has_next_page` or `hasMore`. Pass `next_cursor` unchanged as `cursor` for X reads, draws, extractions, and events. Never replace an extraction cursor with `offset`. Use `after` for `/api/v1/radar` and `afterCursor` for drafts.

Hosted MCP keeps each requested `limit`. REST enforces documented bounds.

Omit `mode` for tweet search, replies, followers, following, and verified
followers. Those operations use automatic maximum coverage. Pass
`next_cursor` back unchanged. Use `mode=standard` only for legacy pagination.

Continue through empty filtered pages while `has_more` is true and the cursor advances. Stop when you reach the requested total or `has_more` becomes false. Treat a missing or repeated `next_cursor` while `has_more` is true as stalled pagination. Return the partial count plus a stop reason.

For advanced nested-reply diagnostics, call
`/api/v1/x/tweets/<tweet_id>/replies?mode=complete&limit=25000`.
Complete mode combines timelines, rankings, cursors, hidden branches, and
search. Direct replies match `inReplyToId`. Keep `nested_replies` separate.
Trust `diagnostic.complete`. HTTP 424 `replies_incomplete` keeps rows.
MCP returns that documented body instead of a tool error.
Inspect `coveragePercentage`, strategy results, cursor failures, missing
modules, `recommendedFallback`, and X-dependent coverage limits.

Errors use `error.type`, `error.code`, and `error.message`, with fields such as `error.retryable` or `error.retry_after` when available. Invalid routes stay blocked without suggesting or calling a different operation. Find the exact method and path in `spec.paths` before retrying. Dependency failures use HTTP `424` in this contract. Fix validation errors before retrying. Respect `retry_after` on `429`.

Write and media results follow the same contract. Read `tweet_id`, `write_action_id`, `charged_credits`, `media_id`, `media_url`, and `message_id` from `response.result`.

Both `search` and `execute` return compact JSON within 24,000 characters. Whitespace inside strings stays unchanged. Native API tools apply the same limit to successful & failed responses.
The limit bounds tool output, not catalog access or available results. Large schema selections can exceed it. Oversized output returns `isError: true` with a recoverable `response_too_large` diagnostic. It includes size, limit, and retry guidance, never partial JSON. An ordinary result containing an `error` field is not a tool failure.
Output errors do not undo completed API actions or charges.
Use REST, SDKs, or extraction exports to store every row.

### Cursor pages and response size

Cursor-based reads may return fewer rows than requested to fit each MCP response.
Continue with `next_cursor` while `has_more` is true.
Deferred rows remain available through that cursor, with their fields intact.
Only rows delivered in each page incur result charges.

Each API response within `execute` must fit within 2 MiB.
A single larger row returns HTTP 413 `response_item_too_large` before result billing.
Retrieve that row through REST.
Projecting fields in `execute` cannot reduce the incoming API response size.
The separate 24,000-character tool output limit still applies.

### Recover oversized results

When saving succeeds, oversized API results include `result_id` and a resource link.
Their `retry` names only tools your catalog lists. Use a retrieval method:

* In Code Mode, pass `result_id` to `execute`, then project or page `xquik.result`.
* In native mode, pass `result_id` to [`readSavedResult`](#readsavedresult) for every page.
* Read `xquik://results/{result_id}` through MCP `resources/read`.

Resources return complete `{ result, warnings? }` JSON.
Return selected fields, fewer rows, shorter strings, or an aggregate from saved data.
Reading saved data makes no API requests and spends no extraction credits.
Explicit `xquik.request()` calls still follow normal billing.
Use the original account and credential scope.
Another account or scope cannot retrieve the result.
Missing or unavailable results return an error.

```json theme={null}
{
  "code": "async () => xquik.result.tweets.slice(0, 10).map(({ id }) => id)",
  "result_id": "<result_id from the output error>"
}
```

Recovery applies only when the response supplies `result_id`.
Every API answer MCP accepts fits in storage. A storage failure may still prevent saving.
Search catalog errors still require a smaller selection.
Without a saved result, contact support before repeating paid requests.
Verify completed writes before retrying them.

### Scope and unavailable operations

The [MCP operation boundary](/mcp/overview#mcp-operation-boundary) lists excluded operations. Guest wallet credential routes and file downloads use REST. Guest keys expose only [eligible paid-read routes](/guides/guest-wallets#eligible-paid-read-routes).

A `402` creates no checkout. Report its `payment_options`, ask the user to choose an amount and option, then wait for explicit confirmation. Full account MCP sessions may call only an advertised account checkout action present in their catalog. Guest wallet creation and top-up remain direct REST after confirmation.

**Examples.**

> Summarize up to 100 tweets with guarded pagination (credits required)

Use `q` for keywords and X search operators, or pass a plain Tweet ID or X
status URL when the agent receives a single stored link.

```javascript theme={null}
async () => {
  const target = 100;
  const query = "from:username giveaway";
  const seenIds = new Set();
  const seenCursors = new Set();
  const sampleTweets = [];
  let cursor;
  let hasMore = true;
  let nextCursor = "";
  let stopReason = "page_cap";

  for (let pageNumber = 0; pageNumber < 10 && seenIds.size < target && hasMore; pageNumber += 1) {
    const { result: page } = await xquik.request({
      path: "/api/v1/x/tweets/search",
      query: {
        q: query,
        limit: String(Math.min(200, target - seenIds.size)),
        ...(cursor ? { cursor } : {}),
      },
    });

    for (const tweet of page.tweets) {
      if (seenIds.has(tweet.id) || seenIds.size >= target) continue;
      seenIds.add(tweet.id);
      if (sampleTweets.length < 20) {
        sampleTweets.push({
          tweet_id: tweet.id,
          text_excerpt: tweet.text?.slice(0, 120) ?? null,
          author_id: tweet.author?.id ?? null,
          author_username: tweet.author?.username ?? null,
          created: tweet["created"] ?? null,
          like_count: tweet.like_count ?? null,
          view_count: tweet.view_count ?? null,
        });
      }
    }

    hasMore = Boolean(page.has_more);
    nextCursor = page.next_cursor ?? "";
    if (!hasMore) {
      stopReason = "exhausted";
      break;
    }
    if (!nextCursor || nextCursor === cursor || seenCursors.has(nextCursor)) {
      stopReason = "cursor_stalled";
      break;
    }
    seenCursors.add(nextCursor);
    cursor = nextCursor;
  }

  return {
    source: "xquik_mcp",
    job: "tweet_search",
    query,
    rows_seen: seenIds.size,
    sample_tweets: sampleTweets,
    has_more: hasMore,
    next_cursor: nextCursor,
    stop_reason: seenIds.size >= target ? "requested_total" : stopReason,
  };
};
```

> Summarize up to 100 followers with guarded pagination (credits required)

```javascript theme={null}
async () => {
  const target = 100;
  const sourceUser = "username";
  const seenIds = new Set();
  const seenCursors = new Set();
  const sampleProfiles = [];
  let verifiedCount = 0;
  let cursor;
  let hasMore = true;
  let nextCursor = "";
  let stopReason = "page_cap";

  for (let pageNumber = 0; pageNumber < 10 && seenIds.size < target && hasMore; pageNumber += 1) {
    const { result: page } = await xquik.request({
      path: `/api/v1/x/users/${sourceUser}/followers`,
      query: {
        pageSize: String(Math.max(20, Math.min(200, target - seenIds.size))),
        ...(cursor ? { cursor } : {}),
      },
    });

    for (const user of page.users) {
      if (seenIds.has(user.id) || seenIds.size >= target) continue;
      seenIds.add(user.id);
      if (user.verified === true) verifiedCount += 1;
      if (sampleProfiles.length < 25) {
        sampleProfiles.push({
          user_id: user.id,
          username: user.username,
          name: user.name ?? null,
          followers: user.followers ?? null,
          verified: user.verified ?? null,
          profile_picture: user.profile_picture ?? null,
        });
      }
    }

    hasMore = Boolean(page.has_more);
    nextCursor = page.next_cursor ?? "";
    if (!hasMore) {
      stopReason = "exhausted";
      break;
    }
    if (!nextCursor || nextCursor === cursor || seenCursors.has(nextCursor)) {
      stopReason = "cursor_stalled";
      break;
    }
    seenCursors.add(nextCursor);
    cursor = nextCursor;
  }

  return {
    source: "xquik_mcp",
    job: "follower_export",
    source_user: sourceUser,
    rows_seen: seenIds.size,
    verified_count: verifiedCount,
    sample_profiles: sampleProfiles,
    has_more: hasMore,
    next_cursor: nextCursor,
    stop_reason: seenIds.size >= target ? "requested_total" : stopReason,
  };
};
```

> Scrape tweet replies to CSV, JSON, or XLSX (credits required)

```javascript theme={null}
async () => {
  const body = {
    toolType: "reply_extractor",
    targetTweetId: "1893704267862470862",
    resultsLimit: 500,
  };

  const { result: extraction } = await xquik.request({
    path: "/api/v1/extractions",
    method: "POST",
    body,
  });

  return {
    source: "xquik_mcp",
    job: "reply_extraction",
    extraction_id: extraction.id,
    status: extraction.status,
    target_tweet_id: body.targetTweetId,
    results_limit: body.resultsLimit,
    poll: `/api/v1/extractions/${extraction.id}`,
    export_csv: `/api/v1/extractions/${extraction.id}/export?format=csv`,
    export_json: `/api/v1/extractions/${extraction.id}/export?format=json`,
    export_xlsx: `/api/v1/extractions/${extraction.id}/export?format=xlsx`,
  };
};
```

> Post a tweet or reply with public media URLs (credits required)

Hosted MCP injects the required `Idempotency-Key`. Direct REST callers must
supply it themselves.

```javascript theme={null}
async () => {
  const body = {
    account: "myxhandle",
    text: "Launch media is ready",
    reply_to_tweet_id: "1893456789012345678",
    media: ["https://example.com/product-demo.mp4"],
  };

  const { result } = await xquik.request({
    path: "/api/v1/x/tweets",
    method: "POST",
    body,
  });

  return {
    source: "xquik_mcp",
    job: "tweet_write",
    status: result.status,
    terminal: result.terminal,
    safe_to_retry: result.safe_to_retry,
    write_action_id: result.id,
    request_hash: result.request.hash,
    tweet_id: result.result?.id ?? result.tweet_id ?? null,
    poll: result.terminal ? null : result.status_url,
    account: body.account,
    reply_to_tweet_id: body.reply_to_tweet_id,
    media: body.media,
    charged: result.billing.charged,
    charged_credits: result.billing.charged_credits,
  };
};
```

> Upload media for a DM (credits required)

```javascript theme={null}
async () => {
  const account = "myxhandle";
  const user_id = "44196397";
  const source_url = "https://example.com/image.png";

  const { result: media } = await xquik.request({
    path: "/api/v1/x/media",
    method: "POST",
    body: {
      account,
      url: source_url,
    },
  });

  const { result: dm } = await xquik.request({
    path: `/api/v1/x/dm/${user_id}`,
    method: "POST",
    body: {
      account,
      text: "Here is the asset",
      media_ids: [media.media_id],
    },
  });

  return {
    source: "xquik_mcp",
    job: "dm_media",
    status: "sent",
    user_id,
    account,
    source_url,
    media_id: media.media_id,
    media_url: media.media_url,
    message_id: dm.message_id,
  };
};
```

Store `message_id` with the uploaded `media_id`. Keep full DM bodies out of
shared MCP outputs. Return IDs, status, media references, and source filenames
instead. Leave `reply_to_message_id` unset because the DM send endpoint rejects
reply threading.

> Download media and get gallery link (credits required)

```javascript theme={null}
async () => {
  const tweet_input = "1234567890";
  const { result: download } = await xquik.request({
    path: "/api/v1/x/media/download",
    method: "POST",
    body: { tweetInput: tweet_input },
  });

  return {
    source: "xquik_mcp",
    job: "media_download",
    mode: "single",
    tweet_input,
    tweet_id: download.tweet_id,
    gallery_url: download.gallery_url,
    cache_hit: download.cache_hit,
    note: "Store gallery_url as the saved media gallery. It is not an uploaded media_id for DMs.",
  };
};
```

> Bulk download: search + download combined

```javascript theme={null}
async () => {
  const query = "from:berktavsan has:videos";
  const { result: search } = await xquik.request({
    path: "/api/v1/x/tweets/search",
    query: { q: query },
  });
  if (!search.tweets?.length) {
    return {
      source: "xquik_mcp",
      job: "bulk_media_download",
      status: "empty",
      query,
    };
  }

  const tweetIds = search.tweets.map((t) => t.id).slice(0, 50);
  const { result: download } = await xquik.request({
    path: "/api/v1/x/media/download",
    method: "POST",
    body: { tweetIds },
  });

  return {
    source: "xquik_mcp",
    job: "bulk_media_download",
    status: "ready",
    query,
    tweet_ids: tweetIds,
    gallery_url: download.gallery_url,
    total_tweets: download.total_tweets,
    total_media: download.total_media,
  };
};
```

> Monitor a user + create webhook (monitor creation requires credits, webhook is free)

```javascript theme={null}
async () => {
  const { result: monitor } = await xquik.request({
    path: "/api/v1/monitors",
    method: "POST",
    body: { username: "elonmusk", eventTypes: ["tweet.new", "tweet.reply"] },
  });
  const { result: webhook } = await xquik.request({
    path: "/api/v1/webhooks",
    method: "POST",
    body: { url: "https://example.com/hook", eventTypes: ["tweet.new", "tweet.reply"] },
  });
  const { result: test } = await xquik.request({
    path: `/api/v1/webhooks/${webhook.id}/test`,
    method: "POST",
  });
  return {
    monitor_id: monitor.id,
    event_types: monitor.event_types,
    next_billing_at: monitor.next_billing_at,
    webhook_id: webhook.id,
    webhook_url: webhook.url,
    save_secret_once:
      "Store webhook.secret for X-Xquik-Signature verification; do not print it in logs.",
    idempotency_keys: ["deliveryId", "streamEventId"],
    delivery_status: `/api/v1/webhooks/${webhook.id}/deliveries`,
    test,
  };
};
```

> Poll stored monitor events (free)

```javascript theme={null}
async () => {
  const monitor_id = "mon_123";
  const event_type = "tweet.new";
  const { result: page } = await xquik.request({
    path: "/api/v1/events",
    query: { monitorId: monitor_id, eventType: event_type },
  });

  return {
    source: "xquik_mcp",
    job: "monitor_event_poll",
    monitor_id,
    event_type,
    rows: page.events.map((event) => ({
      event_id: event.id,
      type: event.type,
      username: event.username ?? null,
      query: event.query ?? null,
      monitor_id: event.monitor_id,
      monitor_type: event.monitor_type,
      occurred_at: event.occurred_at,
      data: event.data,
    })),
    has_more: page.has_more,
    next_cursor: page.next_cursor,
    next_query: page.next_cursor
      ? { monitorId: monitor_id, eventType: event_type, cursor: page.next_cursor }
      : null,
  };
};
```

> Run an extraction with a resumable handoff (credits required)

```javascript theme={null}
async () => {
  const body = {
    toolType: "tweet_search_extractor",
    searchQuery: "launch announcement",
    resultsLimit: 500,
  };

  // Add optional filters only when the user requests them.

  const { result: job } = await xquik.request({
    path: "/api/v1/extractions",
    method: "POST",
    body,
  });

  return {
    source: "xquik_mcp",
    job: "tweet_search_extraction",
    extraction_id: job.id,
    tool_type: job.tool_type,
    status: job.status,
    query: body.searchQuery,
    results_limit: body.resultsLimit,
    poll: `/api/v1/extractions/${job.id}`,
    export_after_complete: `/api/v1/extractions/${job.id}/export?format=json`,
  };
};
```

## Agent handoff patterns

Use extraction exports for generated files. Keep agent handoffs small. Include the job, route, stored row IDs, next cursor, and poll action.
Avoid raw pages when later workers need durable rows.

<CardGroup cols={2}>
  <Card title="Search tweets to JSON" icon="search">
    Call `GET /api/v1/x/tweets/search` with keywords, operators, a Tweet ID, or an X status URL in `q`. Valid time bounds apply to every page. The start is inclusive and the end is exclusive. Store `tweets[].id`, `tweets[].text`, `tweets[].author`, `tweets[].created`, `has_more`, `next_cursor`, and the original `q`. Cost: 1 credit per tweet returned.
  </Card>

  <Card title="Scrape tweet replies to files" icon="messages-square">
    Call `POST /api/v1/extractions` with `reply_extractor` and `targetTweetId`. The create route handles required preflight. Poll `GET /api/v1/extractions/{id}`, export CSV, JSON, or XLSX with `GET /api/v1/extractions/{id}/export`, and store reply rows plus `has_more` and `next_cursor`. Cost: 1 credit per reply extracted or returned.
  </Card>

  <Card title="Export followers to CRM" icon="users">
    Call `GET /api/v1/x/users/{id}/followers` or `POST /api/v1/extractions` with `follower_explorer`. Store `users[].id`, `users[].username`, `users[].name`, `users[].followers`, `has_more`, and `next_cursor`. Cost: 1 credit per follower returned or extracted.
  </Card>

  <Card title="Post media tweets or replies" icon="image">
    Call `POST /api/v1/x/tweets` with `media: ["https://..."]`. Store `tweet_id` or `write_action_id`, `reply_to_tweet_id`, `account`, `charged_credits`, and the original `media` URLs. Cost: 30 credits text-only, plus 2 credits per started MB across attached media.
  </Card>

  <Card title="Send DMs with media" icon="send">
    Call `POST /api/v1/x/media`, then `POST /api/v1/x/dm/{userId}` with one `media_ids` value. Store `media_id`, `media_url`, `message_id`, `user_id`, `account`, and source URL or filename. Keep full DM bodies out of shared outputs and leave `reply_to_message_id` unset. Cost: 10 credits per media upload plus 10 credits per DM send.
  </Card>

  <Card title="Track tweet or reply writes" icon="activity">
    Call `POST /api/v1/x/tweets`, then `GET /api/v1/x/write-actions/{id}` when pending. Store `tweet_id`, `reply_to_tweet_id`, `write_action_id`, `status`, `charged`, `charged_credits`, and `media`. Cost: 30 credits text-only, plus 2 credits per started MB across attached media.
  </Card>

  <Card title="Monitor tweets to webhooks" icon="radio">
    Call `POST /api/v1/monitors` or `POST /api/v1/monitors/keywords`, then `POST /api/v1/webhooks`. Store `monitor.id`, `event_types`, `next_billing_at`, `webhook.id`, webhook URL, and the one-time `webhook.secret`. Run `POST /api/v1/webhooks/{id}/test` before routing production events. Verify `X-Xquik-Signature`. Deduplicate production payloads with `deliveryId` and `streamEventId`. Inspect `GET /api/v1/webhooks/{id}/deliveries` for retry status rows. Each payload contains one monitor event. Process multiple POSTs when one check catches multiple new matching tweets. Cost: 21 credits per active monitor-hour. The rate includes webhook delivery.
  </Card>

  <Card title="Replay monitor events" icon="activity">
    Call `GET /api/v1/events` when a receiver missed webhook delivery or a downstream queue needs replay. Store `event_id`, `type`, `monitor_id`, `monitor_type`, `occurred_at`, `has_more`, and `next_cursor`. Use `cursor` for the next page.
  </Card>
</CardGroup>

Do not upload media before posting tweets or replies when the media is already public. `POST /api/v1/x/tweets` rejects `media_ids` with `400 unsupported_field`. Pass up to 4 public image URLs or exactly 1 public MP4 video URL up to 100 MB in `media` instead. Reserve uploaded `media_id` values for direct messages.

```javascript theme={null}
async () => {
  const { result: page } = await xquik.request({
    path: "/api/v1/x/tweets/search",
    query: { q: "from:username giveaway", limit: "50" },
  });

  return {
    source: "xquik_mcp",
    job: "tweet_search",
    query: "from:username giveaway",
    rows: page.tweets.map((tweet) => ({
      tweet_id: tweet.id,
      text: tweet.text,
      author: tweet.author,
      created: tweet["created"],
      url: tweet.url,
    })),
    has_more: page.has_more,
    next_cursor: page.next_cursor,
  };
};
```

> Look up known tweet IDs

`GET /api/v1/x/tweets` is available in both the full and `paid_reads` catalogs. Send at most 100 tweet IDs.

```javascript theme={null}
async () => {
  const { result: page } = await xquik.request({
    path: "/api/v1/x/tweets",
    query: { ids: "1893456789012345678,1893456789012345679" },
  });
  return {
    tweets: page.tweets.map((tweet) => ({
      tweet_id: tweet.id,
      text: tweet.text,
      author_username: tweet.author?.username ?? null,
    })),
  };
};
```

> Subscribe (free, returns checkout or billing portal URL)

Run this call only after the user explicitly asks to subscribe or open billing.

```javascript theme={null}
async () => {
  return xquik.request({
    path: "/api/v1/subscribe",
    method: "POST",
  });
};
```

## API endpoints

The full MCP catalog groups its operations into 10 categories:

<CardGroup cols={2}>
  <Card title="Account, composition, and credits" icon="user">
    MCP operations across `account`, `composition`, and `credits`: account info, subscribe, X identity, compose, styles, drafts, radar, balance checks, checkout creation, and checkout status.
  </Card>

  <Card title="Extractions and media" icon="download">
    Operations across `extraction` and `media`: giveaway draws, extraction jobs, estimates, and media download.
  </Card>

  <Card title="Monitoring and webhooks" icon="radio">
    Operations in `monitoring`: account monitors, keyword monitors, stored events, webhooks, deliveries, and test delivery.
  </Card>

  <Card title="Support" icon="message-circle">
    Operations in `support`: create, list, read, reply, and close tickets.
  </Card>

  <Card title="Tweets, profiles & followers" icon="search">
    Operations in `twitter`: batch and single tweet lookup, tweet search, article lookup, user Article lists, hidden replies, user lookup, repost checks, follower and following ID lists, creator subscriptions, affiliates, follow checks, trends, bookmarks, notifications, timeline, DM history, likes, media, highlights, followers, replies, communities, lists, and job listings. Timeline and thread results exclude X ads.
  </Card>

  <Card title="X accounts and writes" icon="send">
    Operations across `x-accounts` and `x-write`: connect accounts, resolve challenges, post tweets, like, retweet, follow, remove followers, send and delete DMs, upload media, update profiles, and manage communities.
  </Card>
</CardGroup>

With a guest `paid_reads` key, `search` and `execute` expose only the eligible `twitter` GET operations. The `docs` tool remains available. Use the [guest paid-read route inventory](/guides/guest-wallets#eligible-paid-read-routes) as the public route list.

## Cost summary

<CardGroup cols={2}>
  <Card title="Always free discovery" icon="sparkles">
    `docs` and `search` are free. They return documentation and API contract details.
  </Card>

  <Card title="Free account and stored records" icon="circle-check">
    Compose, cached styles, drafts, radar, subscribe, account, support, credits, X account management, webhooks, stored monitors, stored events, and existing extraction or draw reads are free.
  </Card>

  <Card title="Metered reads and jobs" icon="gauge">
    Tweet search, user lookup, follow checks, media download, trends, extraction creation, and draw creation are metered.
  </Card>

  <Card title="Monitor billing" icon="radio">
    Active monitors cost 21 credits per monitor-hour. Creating one requires enough available credits.
  </Card>

  <Card title="Write actions" icon="send">
    Tweet, reply, like, retweet, follow, DM, profile, community, and media upload writes are metered.
  </Card>

  <Card title="Metered refreshes" icon="refresh-cw">
    Fresh style analysis after the 7-day cache window requires enough available credits.
  </Card>
</CardGroup>

<Warning>
  Never combine free and paid endpoints in a single `Promise.all`. A 402 error on one call fails every result. Call free endpoints first, then paid ones separately.
</Warning>

## Error handling

* **402 / `no_subscription` / `subscription_inactive`.** Report the billing state and available account actions. Existing available credits can still fund metered calls. Ask the user to choose and confirm before calling `POST /api/v1/subscribe`.
* **402 / `no_credits` / `insufficient_credits`.** Report `payment_options`. Full account sessions may create account checkout after confirmation. Guest sessions may explain the direct REST top-up flow, but MCP cannot execute it.
* **429 / `rate_limit_exceeded`.** Respect `error.retry_after`, then retry safe reads with backoff.
* **424 dependency errors.** Report `error.code`, keep partial aggregates, and retry only when `error.retryable` allows it.
* **Validation errors.** Fix the path, query, or body before retrying.

The MCP server never starts subscriptions, checkout, top-up, or other billing mutations in response to an API error. Guest credential routes are never executable through MCP.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.