> ## 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 reference

> MCP tools (explore + xquik) with sandbox API, workflows, and cost breakdown

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

The Xquik MCP server exposes 2 tools: `explore` (search the API spec) and `xquik` (execute API calls). Connect to `https://xquik.com/mcp` with `x-api-key` or OAuth 2.1 Bearer auth. API keys fit terminal clients; OAuth fits browser MCP clients.

***

## explore

Search the API endpoint catalog. Read-only, no network calls, and no credits required. The call still requires MCP authentication through an API key or OAuth Bearer token. Use this to discover available endpoints, check parameters, and find the right API path before executing calls.

**Input:**

<CardGroup cols={2}>
  <Card title="Sandbox code" icon="code">
    Pass `code` as an async arrow function. It is required and can be up to
    10,000 characters.
  </Card>

  <Card title="Spec access" icon="search">
    The function runs against `spec.endpoints` so agents can filter endpoint
    paths, parameters, categories, costs, and response shapes before making a
    live call.
  </Card>
</CardGroup>

**Sandbox API:**

```typescript theme={null}
interface EndpointInfo {
  method: string;
  path: string;
  summary: string;
  category: string; // account, composition, credits, extraction, media, monitoring, support, twitter, x-accounts, x-write
  free: boolean;
  parameters?: Array<{
    name: string;
    in: 'query' | 'path' | 'body';
    required: boolean;
    type: string;
    description: string;
  }>;
  responseShape?: string;
}

declare const spec: { endpoints: EndpointInfo[] };
```

**Examples:**

> Find all free endpoints

```javascript theme={null}
async () => {
  return spec.endpoints.filter(e => e.free);
}
```

> Find endpoints by category

```javascript theme={null}
async () => {
  return spec.endpoints.filter(e => e.category === 'composition');
}
```

> Search by keyword

```javascript theme={null}
async () => {
  return spec.endpoints.filter(e => e.summary.toLowerCase().includes('tweet'));
}
```

***

## xquik

Execute API calls against your Xquik account. The agent writes code using `xquik.request()` with auth injected automatically.

**Input:**

<CardGroup cols={2}>
  <Card title="Sandbox code" icon="code">
    Pass `code` as an async arrow function. It is required and can be up to
    10,000 characters.
  </Card>

  <Card title="Authenticated calls" icon="terminal">
    The function can call `xquik.request(path, { method, body, query })` with
    auth injected automatically.
  </Card>
</CardGroup>

**Sandbox API:**

```typescript theme={null}
declare const xquik: {
  request(path: string, options?: {
    method?: string;  // default: 'GET'
    body?: unknown;
    query?: Record<string, string>;
  }): Promise<unknown>;
};
declare const spec: { endpoints: EndpointInfo[] };
```

**Response contract:**

`xquik.request()` uses the normalized v1 contract automatically. List and search responses use `has_more` and `next_cursor`, even when the REST API page shows the default `has_next_page` field. Pass `next_cursor` back as the `cursor` query parameter for most X data pages. Use `after` instead for `/api/v1/draws`, `/api/v1/extractions`, `/api/v1/events`, and `/api/v1/radar`.

Write and media responses also use the MCP-normalized snake\_case contract. Read `tweet_id`, `write_action_id`, `charged_credits`, `media_id`, `media_url`, and `message_id` from `xquik.request()` results. REST and generated SDK pages may show camelCase fields such as `tweetId`, `writeActionId`, `chargedCredits`, `mediaId`, and `messageId`; keep MCP agents on snake\_case when reading tool results.

**Workflow Examples:**

> Compose an algorithm-optimized tweet (3-step, free)

```javascript theme={null}
async () => {
  // Step 1: Get algorithm data and follow-up questions
  const compose = await xquik.request('/api/v1/compose', {
    method: 'POST',
    body: { step: 'compose', topic: 'AI agents' }
  });
  return compose;
  // Returns contentRules, followUpQuestions, scorerWeights
  // Step 2: After user answers: { step: 'refine', goal, tone, topic }
  // Step 3: After drafting: { step: 'score', draft }
  // The score step runs algorithm checks. The draft must pass
  // all checks before presenting to the user.
}
```

> Save a writing style from screenshots (free)

```javascript theme={null}
async () => {
  // When a user shares tweet screenshots, extract the texts and save as a style.
  // This lets free users clone any writing voice without a subscription.
  return xquik.request('/api/v1/styles/elonmusk', {
    method: 'PUT',
    body: {
      label: 'Elon Musk style',
      tweets: [
        { text: 'The most entertaining outcome is the most likely' },
        { text: 'Mars, here we come!!' }
      ]
    }
  });
  // Then compose with: POST /api/v1/compose { step: 'compose', topic: '...', styleUsername: 'elonmusk' }
}
```

> Browse trending news from radar (free)

```javascript theme={null}
async () => {
  return xquik.request('/api/v1/radar');
}
```

> Radar + Style + Compose combined (free)

```javascript theme={null}
async () => {
  const [radar, styles] = await Promise.all([
    xquik.request('/api/v1/radar'),
    xquik.request('/api/v1/styles'),
  ]);
  return { radar, styles };
}
```

> Analyze a user's writing style

```javascript theme={null}
async () => {
  // Returns cached style if available (free)
  // Auto-refreshes from X if cache older than 7 days (subscription required)
  return xquik.request('/api/v1/styles', {
    method: 'POST',
    body: { username: 'elonmusk' }
  });
}
```

> Search tweets with cursor pagination (subscription 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 query = 'from:xquikcom giveaway';
  const rows = [];
  let cursor;
  let hasMore = false;
  let nextCursor = '';

  for (let pageNumber = 0; pageNumber < 2; pageNumber += 1) {
    const page = await xquik.request('/api/v1/x/tweets/search', {
      query: {
        q: query,
        ...(cursor ? { cursor } : {})
      }
    });

    rows.push(...page.tweets.map(tweet => ({
      source: 'xquik_mcp',
      job: 'tweet_search',
      query,
      tweet_id: tweet.id,
      text: tweet.text,
      author_id: tweet.author?.id ?? null,
      author_username: tweet.author?.username ?? null,
      created: tweet['created'] ?? null,
      url: tweet.url ?? null,
      like_count: tweet.like_count ?? null,
      reply_count: tweet.reply_count ?? null,
      retweet_count: tweet.retweet_count ?? null
    })));
    hasMore = Boolean(page.has_more);
    nextCursor = page.next_cursor ?? '';
    if (!hasMore || nextCursor === '') break;
    cursor = nextCursor;
  }

  return {
    source: 'xquik_mcp',
    job: 'tweet_search',
    rows,
    has_more: hasMore,
    next_cursor: nextCursor
  };
}
```

> List follower pages (subscription required)

```javascript theme={null}
async () => {
  const sourceUser = 'xquikcom';
  const rows = [];
  let cursor;
  let hasMore = false;
  let nextCursor = '';

  for (let pageNumber = 0; pageNumber < 2; pageNumber += 1) {
    const page = await xquik.request(`/api/v1/x/users/${sourceUser}/followers`, {
      query: {
        pageSize: '50',
        ...(cursor ? { cursor } : {})
      }
    });

    rows.push(...page.users.map(user => ({
      source: 'xquik_mcp',
      job: 'follower_export',
      source_user: sourceUser,
      user_id: user.id,
      username: user.username,
      name: user.name ?? null,
      followers: user.followers ?? null,
      following: user.following ?? null,
      verified: user.verified ?? null,
      profile_picture: user.profile_picture ?? null,
      page_index: pageNumber
    })));
    hasMore = Boolean(page.has_more);
    nextCursor = page.next_cursor ?? '';
    if (!hasMore || nextCursor === '') break;
    cursor = nextCursor;
  }

  return {
    source: 'xquik_mcp',
    job: 'follower_export',
    source_user: sourceUser,
    rows,
    has_more: hasMore,
    next_cursor: nextCursor
  };
}
```

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

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

  const estimate = await xquik.request('/api/v1/extractions/estimate', {
    method: 'POST',
    body
  });

  if (estimate.allowed === false) {
    return {
      source: 'xquik_mcp',
      job: 'reply_extraction',
      status: 'blocked',
      error: estimate.error,
      credits_required: estimate.credits_required,
      credits_available: estimate.credits_available
    };
  }

  const extraction = await xquik.request('/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,
    estimated_results: estimate.estimated_results,
    credits_required: estimate.credits_required,
    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 (subscription required)

```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('/api/v1/x/tweets', {
    method: 'POST',
    body
  });

  if (result.error === 'x_write_unconfirmed') {
    return {
      source: 'xquik_mcp',
      job: 'tweet_write',
      status: result.status,
      write_action_id: result.write_action_id,
      charged: result.charged,
      retryable: result.retryable,
      poll: `/api/v1/x/write-actions/${result.write_action_id}`,
      account: body.account,
      reply_to_tweet_id: body.reply_to_tweet_id,
      media: body.media
    };
  }

  return {
    source: 'xquik_mcp',
    job: 'tweet_write',
    status: 'posted',
    tweet_id: result.tweet_id,
    account: body.account,
    reply_to_tweet_id: body.reply_to_tweet_id,
    media: body.media,
    charged: true
  };
}
```

> Upload media for a DM (subscription required)

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

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

  const dm = await xquik.request(`/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 (subscription required)

```javascript theme={null}
async () => {
  const tweet_input = '1234567890';
  const download = await xquik.request('/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 search = await xquik.request('/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 download = await xquik.request('/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 subscription, webhook is free)

```javascript theme={null}
async () => {
  const monitor = await xquik.request('/api/v1/monitors', {
    method: 'POST',
    body: { username: 'elonmusk', eventTypes: ['tweet.new', 'tweet.reply'] }
  });
  const webhook = await xquik.request('/api/v1/webhooks', {
    method: 'POST',
    body: { url: 'https://example.com/hook', eventTypes: ['tweet.new', 'tweet.reply'] }
  });
  const test = await xquik.request(`/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 page = await xquik.request('/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, after: page.next_cursor }
      : null
  };
}
```

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

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

  const estimate = await xquik.request('/api/v1/extractions/estimate', {
    method: 'POST',
    body
  });

  if (estimate.allowed === false) {
    return {
      source: 'xquik_mcp',
      job: 'tweet_search_extraction',
      status: 'blocked',
      error: estimate.error,
      credits_required: estimate.credits_required,
      credits_available: estimate.credits_available
    };
  }

  const job = await xquik.request('/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,
    estimated_results: estimate.estimated_results,
    credits_required: estimate.credits_required,
    poll: `/api/v1/extractions/${job.id}`,
    export_after_complete: `/api/v1/extractions/${job.id}/export?format=json`
  };
}
```

## Agent handoff patterns

MCP returns JSON. Use extraction export endpoints when you need Xquik to generate
CSV, JSON, XLSX, Markdown, or PDF files. For agent queues, CRMs, and
warehouses, return a small object with the original job, the route used,
normalized rows or IDs to store, and the next cursor or write action to poll.
Avoid returning raw `tweets` or `users` pages when the next agent or worker
needs durable handoff 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`. 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/estimate`, then `POST /api/v1/extractions` with `reply_extractor` and `targetTweetId`. Poll `GET /api/v1/extractions/{id}`, export CSV/JSON/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`, de-dupe production payloads with `deliveryId` and `streamEventId`, and inspect `GET /api/v1/webhooks/{id}/deliveries` for retry status rows. Each payload contains one monitor event, so process multiple POSTs when one check catches multiple new matching tweets. Cost: 21 credits per active monitor-hour; webhook delivery is included.
  </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 `after` for the next page. Do not use `cursor` on event pages.
  </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 page = await xquik.request('/api/v1/x/tweets/search', {
    query: { q: 'from:xquikcom giveaway', limit: '50' }
  });

  return {
    source: 'xquik_mcp',
    job: 'tweet_search',
    query: 'from:xquikcom 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
  };
}
```

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

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

***

## API endpoints

The server covers 120 operations across 10 categories:

<CardGroup cols={2}>
  <Card title="Account, composition, and credits" icon="user">
    24 operations across `account`, `composition`, and `credits`: account info, API keys, subscribe, X identity, compose, styles, drafts, radar, balance checks, top-ups, checkout status, and quick top-up.
  </Card>

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

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

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

  <Card title="X data reads" icon="search">
    38 operations in `twitter`: tweet search, tweet and article lookup, user lookup, follow checks, trends, bookmarks, notifications, timeline, DM history, likes, media, followers, replies, communities, and lists.
  </Card>

  <Card title="X accounts and writes" icon="send">
    25 operations across `x-accounts` and `x-write`: connect accounts, retry connection issues, post tweets, like, retweet, follow, remove followers, send DMs, upload media, update profiles, and manage community membership.
  </Card>
</CardGroup>

Use `explore` to browse the full catalog with parameters and response shapes.

***

## Cost summary

<CardGroup cols={2}>
  <Card title="Always free discovery" icon="sparkles">
    `explore` is free. Use it to find endpoints, parameters, and response shapes before making API calls.
  </Card>

  <Card title="Free account and stored records" icon="circle-check">
    Compose, cached styles, drafts, radar, subscribe, account, API keys, 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 instant monitors cost 21 credits per active monitor-hour. Creating monitors requires a subscription and 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="Subscription-only refreshes" icon="refresh-cw">
    Fresh style analysis after the 7-day cache window requires an active subscription.
  </Card>
</CardGroup>

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

***

## Error handling

* **402 / `no_subscription` / `subscription_inactive`**: The sandbox attempts `POST /api/v1/subscribe`; when it returns a URL, the error includes it. Present that URL to the user.
* **402 / `no_credits` / `insufficient_credits`**: Free data already fetched is preserved. Call `POST /api/v1/credits/topup` or `POST /api/v1/credits/quick-topup`, then retry the failed metered call.
* **429**: Rate limited. Retry with exponential backoff.
* **API errors**: Include status code and message in the response.

***
