From b31cc0b96d84303522f3c98ba7d9b7b3b33b7359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Bay=C4=B1r?= Date: Sun, 29 Mar 2026 18:54:39 +0300 Subject: [PATCH] Add x-twitter-scraper skill: X (Twitter) data platform for AI agents 120 REST API endpoints, 2 MCP tools, HMAC webhooks. Covers tweet search, user lookup, follower extraction, write actions, monitoring, giveaway draws, trending topics. Reads from $0.00015/call. Co-Authored-By: Claude Opus 4.6 (1M context) --- skills/.curated/x-twitter-scraper/LICENSE.txt | 21 + skills/.curated/x-twitter-scraper/SKILL.md | 710 ++++++++ .../references/api-endpoints.md | 1595 +++++++++++++++++ .../references/extractions.md | 229 +++ .../x-twitter-scraper/references/mcp-setup.md | 201 +++ .../x-twitter-scraper/references/mcp-tools.md | 151 ++ .../references/python-examples.md | 151 ++ .../x-twitter-scraper/references/types.md | 1163 ++++++++++++ .../x-twitter-scraper/references/webhooks.md | 195 ++ 9 files changed, 4416 insertions(+) create mode 100644 skills/.curated/x-twitter-scraper/LICENSE.txt create mode 100644 skills/.curated/x-twitter-scraper/SKILL.md create mode 100644 skills/.curated/x-twitter-scraper/references/api-endpoints.md create mode 100644 skills/.curated/x-twitter-scraper/references/extractions.md create mode 100644 skills/.curated/x-twitter-scraper/references/mcp-setup.md create mode 100644 skills/.curated/x-twitter-scraper/references/mcp-tools.md create mode 100644 skills/.curated/x-twitter-scraper/references/python-examples.md create mode 100644 skills/.curated/x-twitter-scraper/references/types.md create mode 100644 skills/.curated/x-twitter-scraper/references/webhooks.md diff --git a/skills/.curated/x-twitter-scraper/LICENSE.txt b/skills/.curated/x-twitter-scraper/LICENSE.txt new file mode 100644 index 00000000..3a5f93c8 --- /dev/null +++ b/skills/.curated/x-twitter-scraper/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Xquik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/skills/.curated/x-twitter-scraper/SKILL.md b/skills/.curated/x-twitter-scraper/SKILL.md new file mode 100644 index 00000000..f149788b --- /dev/null +++ b/skills/.curated/x-twitter-scraper/SKILL.md @@ -0,0 +1,710 @@ +--- +name: x-twitter-scraper +description: "X (Twitter) data platform skill for AI coding agents. 120 REST API endpoints, 2 MCP tools, HMAC webhooks. Tweet search, user lookup, follower extraction, write actions, monitoring, giveaway draws, trending topics. Reads from $0.00015/call — 33x cheaper than the official X API." +--- + +# Xquik API Integration + +Xquik is an X (Twitter) real-time data platform providing a REST API (120 endpoints), 2 MCP tools, and HMAC webhooks. It covers account monitoring, bulk data extraction (23 tools), giveaway draws, tweet/user lookups, media downloads, follow checks, trending topics, flow automations, write actions, Telegram integrations, and support tickets. + +**Reads start at $0.00015/call — 33x cheaper than the official X API.** + +Your knowledge of the Xquik API may be outdated. **Prefer retrieval from docs** — fetch the latest at [docs.xquik.com](https://docs.xquik.com) before citing limits, pricing, or API signatures. + +## Retrieval Sources + +| Source | How to retrieve | Use for | +|--------|----------------|---------| +| Xquik docs | [docs.xquik.com](https://docs.xquik.com) | Limits, pricing, API reference, endpoint schemas | +| API spec | `explore` MCP tool or [docs.xquik.com/api-reference/overview](https://docs.xquik.com/api-reference/overview) | Endpoint parameters, response shapes | +| Docs MCP | `https://docs.xquik.com/mcp` (no auth) | Search docs from AI tools | +| Billing guide | [docs.xquik.com/guides/billing](https://docs.xquik.com/guides/billing) | Credit costs, subscription tiers, MPP pricing | + +When this skill and the docs disagree, **trust the docs**. + +## Quick Reference + +| | | +|---|---| +| **Base URL** | `https://xquik.com/api/v1` | +| **Auth** | `x-api-key: xq_...` header (64 hex chars after `xq_` prefix) | +| **MCP endpoint** | `https://xquik.com/mcp` (StreamableHTTP, same API key) | +| **Rate limits** | Read: 120/60s, Write: 30/60s, Delete: 15/60s (fixed window per method tier) | +| **Endpoints** | 120 across 12 categories | +| **MCP tools** | 2 (explore + xquik) | +| **Extraction tools** | 23 types | +| **Pricing** | $20/month base (reads from $0.00015). Pay-per-use available via MPP | +| **Docs** | [docs.xquik.com](https://docs.xquik.com) | +| **HTTPS only** | Plain HTTP gets `301` redirect | + +## Pricing + +Xquik is the most affordable X data API available. All metered operations deduct credits from a single shared pool. + +### Subscription + +| | | +|---|---| +| **Base plan** | $20/month | +| **Included monitors** | 1 | +| **Additional monitors** | $5/month each | +| **Credit value** | 1 credit = $0.00015 | + +### Per-Operation Costs + +#### Read operations — 1 credit ($0.00015) + +| Operation | Unit | +|-----------|------| +| Get tweet | per call | +| Search tweets | per tweet returned | +| User tweets | per tweet returned | +| User likes | per result | +| User media | per result | +| Bookmarks | per result | +| Bookmark folders | per call | +| Notifications | per result | +| Timeline | per result | +| DM history | per result | +| Download media | per media item | + +#### Read operations — 2 credits ($0.0003) + +| Operation | Unit | +|-----------|------| +| Get user | per call | +| Tweet favoriters | per result | +| Followers you know | per result | +| Verified followers | per result | + +#### Read operations — 3 credits ($0.00045) + +| Operation | Unit | +|-----------|------| +| Trends | per call | + +#### Read operations — 7 credits ($0.00105) + +| Operation | Unit | +|-----------|------| +| Follow check | per call | +| Get article | per call | + +#### Write operations — 2 credits ($0.0003) + +All write actions: create/delete tweet, like, unlike, retweet, follow, unfollow, send DM, update profile/avatar/banner, upload media, community actions. + +#### Extractions & draws + +Draws: 1 credit per participant. Extraction cost depends on the tool type: + +| Credits/result | Extraction types | +|----------------|-----------------| +| 1 | Tweets, replies, quotes, mentions, posts, likes, media, tweet search | +| 2 | Followers, following, verified followers, favoriters, retweeters, community members, people search, list members, list followers | +| 7 | Articles | + +#### Free operations ($0) + +Monitors, webhooks, integrations, account status, radar (7 sources), extraction/draw history, cost estimates, tweet composition (compose, refine, score), style cache management, drafts, support tickets, API key management, X account management. + +### Price Comparison vs Official X API + +| | Xquik | X API Basic | X API Pro | +|---|---|---|---| +| **Monthly cost** | **$20** | $100 | $5,000 | +| **Cost per tweet read** | **$0.00015** | ~$0.01 | ~$0.005 | +| **Cost per user lookup** | **$0.0003** | ~$0.01 | ~$0.005 | +| **Write actions** | **$0.0003** | Limited | Limited | +| **Bulk extraction** | **$0.00015/result** | Not available | Not available | +| **Monitoring + webhooks** | **Free** | Not available | Not available | +| **Giveaway draws** | **$0.00015/entry** | Not available | Not available | + +### Pay-Per-Use + +Two options without a monthly subscription: + +**Credits (Stripe)**: Top up credits via `POST /credits/topup` ($10 minimum). 1 credit = $0.00015. Works with all 120 endpoints. + +**MPP (USDC)**: 16 X-API endpoints accept anonymous payments via Tempo (USDC). No account needed. + +| Endpoint | Price | Unit | +|----------|-------|------| +| `GET /x/tweets/{id}` | $0.00015 | per call | +| `GET /x/tweets/search` | $0.00015 | per tweet | +| `GET /x/tweets/{id}/quotes` | $0.00015 | per tweet | +| `GET /x/tweets/{id}/replies` | $0.00015 | per tweet | +| `GET /x/tweets/{id}/retweeters` | $0.00015 | per user | +| `GET /x/tweets/{id}/favoriters` | $0.00015 | per user | +| `GET /x/tweets/{id}/thread` | $0.00015 | per tweet | +| `GET /x/users/{id}` | $0.00015 | per call | +| `GET /x/users/{id}/tweets` | $0.00015 | per tweet | +| `GET /x/users/{id}/likes` | $0.00015 | per tweet | +| `GET /x/users/{id}/media` | $0.00015 | per tweet | +| `GET /x/followers/check` | $0.00105 | per call | +| `GET /x/articles/{tweetId}` | $0.00105 | per call | +| `POST /x/media/download` | $0.00015 | per media item | +| `GET /x/trends` | $0.00045 | per call | +| `GET /trends` | $0.00045 | per call | + +SDK: `npm i mppx` (TypeScript). Handles the 402 challenge/credential flow automatically. + +### Credits + +Prepaid credits for metered operations. 1 credit = $0.00015. Top up via `POST /credits/topup` ($10 minimum). + +Check balance: `GET /credits` — returns `balance`, `lifetimePurchased`, `lifetimeUsed`. + +### Extra Usage + +Enable from dashboard to continue metered calls beyond included allowance. Tiered spending limits: $5 → $7 → $10 → $15 → $25 (increases with each paid overage invoice). + +## Quick Decision Trees + +### "I need X data" + +``` +Need X data? +├─ Single tweet by ID or URL → GET /x/tweets/{id} +├─ Full X Article by tweet ID → GET /x/articles/{id} +├─ Search tweets by keyword → GET /x/tweets/search +├─ User profile by username → GET /x/users/{username} +├─ User's recent tweets → GET /x/users/{id}/tweets +├─ User's liked tweets → GET /x/users/{id}/likes +├─ User's media tweets → GET /x/users/{id}/media +├─ Tweet favoriters (who liked) → GET /x/tweets/{id}/favoriters +├─ Mutual followers → GET /x/users/{id}/followers-you-know +├─ Check follow relationship → GET /x/followers/check +├─ Download media (images/video) → POST /x/media/download +├─ Trending topics (X) → GET /trends +├─ Trending news (7 sources, free) → GET /radar +├─ Bookmarks → GET /x/bookmarks +├─ Notifications → GET /x/notifications +├─ Home timeline → GET /x/timeline +└─ DM conversation history → GET /x/dm/{userId}/history +``` + +### "I need bulk extraction" + +``` +Need bulk data? +├─ Replies to a tweet → reply_extractor +├─ Retweets of a tweet → repost_extractor +├─ Quotes of a tweet → quote_extractor +├─ Favoriters of a tweet → favoriters +├─ Full thread → thread_extractor +├─ Article content → article_extractor +├─ User's liked tweets (bulk) → user_likes +├─ User's media tweets (bulk) → user_media +├─ Account followers → follower_explorer +├─ Account following → following_explorer +├─ Verified followers → verified_follower_explorer +├─ Mentions of account → mention_extractor +├─ Posts from account → post_extractor +├─ Community members → community_extractor +├─ Community moderators → community_moderator_explorer +├─ Community posts → community_post_extractor +├─ Community search → community_search +├─ List members → list_member_extractor +├─ List posts → list_post_extractor +├─ List followers → list_follower_explorer +├─ Space participants → space_explorer +├─ People search → people_search +└─ Tweet search (bulk, up to 1K) → tweet_search_extractor +``` + +### "I need to write/post" + +``` +Need write actions? +├─ Post a tweet → POST /x/tweets +├─ Delete a tweet → DELETE /x/tweets/{id} +├─ Like a tweet → POST /x/tweets/{id}/like +├─ Unlike a tweet → DELETE /x/tweets/{id}/like +├─ Retweet → POST /x/tweets/{id}/retweet +├─ Follow a user → POST /x/users/{id}/follow +├─ Unfollow a user → DELETE /x/users/{id}/follow +├─ Send a DM → POST /x/dm/{userId} +├─ Update profile → PATCH /x/profile +├─ Update avatar → PATCH /x/profile/avatar +├─ Update banner → PATCH /x/profile/banner +├─ Upload media → POST /x/media +├─ Create community → POST /x/communities +├─ Join community → POST /x/communities/{id}/join +└─ Leave community → DELETE /x/communities/{id}/join +``` + +### "I need monitoring & alerts" + +``` +Need real-time monitoring? +├─ Monitor an account → POST /monitors +├─ Poll for events → GET /events +├─ Receive events via webhook → POST /webhooks +├─ Receive events via Telegram → POST /integrations +└─ Automate workflows → POST /automations +``` + +### "I need AI composition" + +``` +Need help writing tweets? +├─ Compose algorithm-optimized tweet → POST /compose (step=compose) +├─ Refine with goal + tone → POST /compose (step=refine) +├─ Score against algorithm → POST /compose (step=score) +├─ Analyze tweet style → POST /styles +├─ Compare two styles → GET /styles/compare +├─ Track engagement metrics → GET /styles/{username}/performance +└─ Save draft → POST /drafts +``` + +## Authentication + +Every request requires an API key via the `x-api-key` header. Keys start with `xq_` and are generated from the Xquik dashboard. The key is shown only once at creation; store it securely. + +```javascript +const API_KEY = "xq_YOUR_KEY_HERE"; +const BASE = "https://xquik.com/api/v1"; +const headers = { "x-api-key": API_KEY, "Content-Type": "application/json" }; +``` + +For Python examples, see [references/python-examples.md](references/python-examples.md). + +## Choosing the Right Endpoint + +| Goal | Endpoint | Cost | +|------|----------|------| +| **Get a single tweet** by ID/URL | `GET /x/tweets/{id}` | 1 credit | +| **Get an X Article** by tweet ID | `GET /x/articles/{id}` | 7 credits | +| **Search tweets** by keyword/hashtag | `GET /x/tweets/search?q=...` | 1 credit/tweet | +| **Get a user profile** | `GET /x/users/{username}` | 2 credits | +| **Get user's recent tweets** | `GET /x/users/{id}/tweets` | 1 credit/tweet | +| **Get user's liked tweets** | `GET /x/users/{id}/likes` | 1 credit/result | +| **Get user's media tweets** | `GET /x/users/{id}/media` | 1 credit/result | +| **Get tweet favoriters** | `GET /x/tweets/{id}/favoriters` | 2 credits/result | +| **Get mutual followers** | `GET /x/users/{id}/followers-you-know` | 2 credits/result | +| **Check follow relationship** | `GET /x/followers/check?source=A&target=B` | 7 credits | +| **Get trending topics** | `GET /trends?woeid=1` | 3 credits | +| **Get radar (trending news)** | `GET /radar?source=hacker_news` | Free | +| **Get bookmarks** | `GET /x/bookmarks` | 1 credit/result | +| **Get bookmark folders** | `GET /x/bookmarks/folders` | 1 credit | +| **Get notifications** | `GET /x/notifications` | 1 credit/result | +| **Get home timeline** | `GET /x/timeline` | 1 credit/result | +| **Get DM history** | `GET /x/dm/{userId}/history` | 1 credit/result | +| **Monitor an X account** | `POST /monitors` | Free | +| **Update monitor event types** | `PATCH /monitors/{id}` | Free | +| **Poll for events** | `GET /events` | Free | +| **Receive events in real time** | `POST /webhooks` | Free | +| **Update webhook** | `PATCH /webhooks/{id}` | Free | +| **Run a giveaway draw** | `POST /draws` | 1 credit/entry | +| **Download tweet media** | `POST /x/media/download` | 1 credit/item | +| **Extract bulk data** | `POST /extractions` | 1-7 credits/result | +| **Check credits** | `GET /credits` | Free | +| **Top up credits** | `POST /credits/topup` | Free | +| **Check account/usage** | `GET /account` | Free | +| **Link your X identity** | `PUT /account/x-identity` | Free | +| **Analyze tweet style** | `POST /styles` | Metered | +| **Save custom style** | `PUT /styles/{username}` | Free | +| **Get cached style** | `GET /styles/{username}` | Free | +| **Compare styles** | `GET /styles/compare?username1=A&username2=B` | Free | +| **Get tweet performance** | `GET /styles/{username}/performance` | Metered | +| **Save a tweet draft** | `POST /drafts` | Free | +| **List/manage drafts** | `GET /drafts`, `DELETE /drafts/{id}` | Free | +| **Compose a tweet** | `POST /compose` | Free | +| **Connect an X account** | `POST /x/accounts` | Free | +| **List connected accounts** | `GET /x/accounts` | Free | +| **Re-authenticate account** | `POST /x/accounts/{id}/reauth` | Free | +| **Post a tweet** | `POST /x/tweets` | 2 credits | +| **Delete a tweet** | `DELETE /x/tweets/{id}` | 2 credits | +| **Like / Unlike a tweet** | `POST` / `DELETE /x/tweets/{id}/like` | 2 credits | +| **Retweet** | `POST /x/tweets/{id}/retweet` | 2 credits | +| **Follow / Unfollow a user** | `POST` / `DELETE /x/users/{id}/follow` | 2 credits | +| **Send a DM** | `POST /x/dm/{userId}` | 2 credits | +| **Update profile** | `PATCH /x/profile` | 2 credits | +| **Update avatar** | `PATCH /x/profile/avatar` | 2 credits | +| **Update banner** | `PATCH /x/profile/banner` | 2 credits | +| **Upload media** | `POST /x/media` | 2 credits | +| **Community actions** | `POST /x/communities`, `POST /x/communities/{id}/join` | 2 credits | +| **Create Telegram integration** | `POST /integrations` | Free | +| **Manage integrations** | `GET /integrations`, `PATCH /integrations/{id}` | Free | +| **Create automation flow** | `POST /automations` | Free | +| **Manage automation flows** | `GET /automations`, `PATCH /automations/{slug}` | Free | +| **Add automation steps** | `POST /automations/{slug}/steps` | Free | +| **Trigger flow via webhook** | `POST /webhooks/inbound/{token}` | Free | +| **Open support ticket** | `POST /support/tickets` | Free | +| **Manage support tickets** | `GET /support/tickets`, `POST /support/tickets/{id}/messages` | Free | + +## Error Handling & Retry + +All errors return `{ "error": "error_code" }`. Key error codes: + +| Status | Code | Action | +|--------|------|--------| +| 400 | `invalid_input`, `invalid_id`, `invalid_params`, `invalid_tweet_url`, `invalid_tweet_id`, `invalid_username`, `invalid_tool_type`, `invalid_format`, `missing_query`, `missing_params`, `webhook_inactive`, `no_media` | Fix the request, do not retry | +| 401 | `unauthenticated` | Check API key | +| 402 | `no_subscription`, `subscription_inactive`, `usage_limit_reached`, `no_addon`, `extra_usage_disabled`, `extra_usage_requires_v2`, `frozen`, `overage_limit_reached`, `insufficient_credits` | Subscribe, top up credits, enable extra usage, or wait for quota reset | +| 403 | `monitor_limit_reached`, `api_key_limit_reached`, `flow_limit_reached`, `step_limit_reached` | Delete a monitor/key/flow or add capacity | +| 404 | `not_found`, `user_not_found`, `tweet_not_found`, `style_not_found`, `draft_not_found`, `account_not_found` | Resource doesn't exist or belongs to another account | +| 403 | `account_needs_reauth` | Connected X account needs re-authentication | +| 409 | `monitor_already_exists`, `account_already_connected`, `conflict` | Resource already exists or concurrent edit conflict | +| 422 | `login_failed` | X credential verification failed. Check credentials | +| 429 | `x_api_rate_limited` | Rate limited. Retry with exponential backoff, respect `Retry-After` header | +| 500 | `internal_error` | Retry with backoff | +| 502 | `stream_registration_failed`, `x_api_unavailable`, `x_api_unauthorized`, `delivery_failed` | Retry with backoff | + +Retry only `429` and `5xx`. Never retry `4xx` (except 429). Max 3 retries with exponential backoff: + +```javascript +async function xquikFetch(path, options = {}) { + const baseDelay = 1000; + + for (let attempt = 0; attempt <= 3; attempt++) { + const response = await fetch(`${BASE}${path}`, { + ...options, + headers: { ...headers, ...options.headers }, + }); + + if (response.ok) return response.json(); + + const retryable = response.status === 429 || response.status >= 500; + if (!retryable || attempt === 3) { + const error = await response.json(); + throw new Error(`Xquik API ${response.status}: ${error.error}`); + } + + const retryAfter = response.headers.get("Retry-After"); + const delay = retryAfter + ? parseInt(retryAfter, 10) * 1000 + : baseDelay * Math.pow(2, attempt) + Math.random() * 1000; + + await new Promise((resolve) => setTimeout(resolve, delay)); + } +} +``` + +## Cursor Pagination + +Events, draws, extractions, and extraction results use cursor-based pagination. When more results exist, the response includes `hasMore: true` and a `nextCursor` string. Pass `nextCursor` as the `after` query parameter. + +```javascript +async function fetchAllPages(path, dataKey) { + const results = []; + let cursor; + + while (true) { + const params = new URLSearchParams({ limit: "100" }); + if (cursor) params.set("after", cursor); + + const data = await xquikFetch(`${path}?${params}`); + results.push(...data[dataKey]); + + if (!data.hasMore) break; + cursor = data.nextCursor; + } + + return results; +} +``` + +Cursors are opaque strings. Never decode or construct them manually. + +## Extraction Tools (23 Types) + +Extractions run bulk data collection jobs. The complete workflow: estimate cost, create job, retrieve results, optionally export. + +### Tool Types and Required Parameters + +| Tool Type | Required Field | Description | Cost | +|-----------|---------------|-------------|------| +| `reply_extractor` | `targetTweetId` | Users who replied to a tweet | 1 credit/result | +| `repost_extractor` | `targetTweetId` | Users who retweeted a tweet | 2 credits/result | +| `quote_extractor` | `targetTweetId` | Users who quote-tweeted a tweet | 1 credit/result | +| `thread_extractor` | `targetTweetId` | All tweets in a thread | 1 credit/result | +| `article_extractor` | `targetTweetId` | Article content linked in a tweet | 7 credits/result | +| `favoriters` | `targetTweetId` | Users who favorited a tweet | 2 credits/result | +| `follower_explorer` | `targetUsername` | Followers of an account | 2 credits/result | +| `following_explorer` | `targetUsername` | Accounts followed by a user | 2 credits/result | +| `verified_follower_explorer` | `targetUsername` | Verified followers of an account | 2 credits/result | +| `mention_extractor` | `targetUsername` | Tweets mentioning an account | 1 credit/result | +| `post_extractor` | `targetUsername` | Posts from an account | 1 credit/result | +| `user_likes` | `targetUserId` | Tweets liked by a user | 1 credit/result | +| `user_media` | `targetUserId` | Media tweets from a user | 1 credit/result | +| `community_extractor` | `targetCommunityId` | Members of a community | 2 credits/result | +| `community_moderator_explorer` | `targetCommunityId` | Moderators of a community | 2 credits/result | +| `community_post_extractor` | `targetCommunityId` | Posts from a community | 1 credit/result | +| `community_search` | `targetCommunityId` + `searchQuery` | Search posts within a community | 1 credit/result | +| `list_member_extractor` | `targetListId` | Members of a list | 2 credits/result | +| `list_post_extractor` | `targetListId` | Posts from a list | 1 credit/result | +| `list_follower_explorer` | `targetListId` | Followers of a list | 2 credits/result | +| `space_explorer` | `targetSpaceId` | Participants of a Space | 2 credits/result | +| `people_search` | `searchQuery` | Search for users by keyword | 2 credits/result | +| `tweet_search_extractor` | `searchQuery` | Search and extract tweets by keyword or hashtag (bulk, up to 1,000) | 1 credit/result | + +### Complete Extraction Workflow + +```javascript +// Step 1: Estimate cost before running (pass resultsLimit if you only need a sample) +const estimate = await xquikFetch("/extractions/estimate", { + method: "POST", + body: JSON.stringify({ + toolType: "follower_explorer", + targetUsername: "elonmusk", + resultsLimit: 1000, // optional: limit to 1,000 results instead of all + }), +}); +// Response: { allowed: true, estimatedResults: 195000000, usagePercent: 12, projectedPercent: 98 } + +if (!estimate.allowed) { + console.log("Extraction would exceed monthly quota"); + return; +} + +// Step 2: Create extraction job (pass same resultsLimit to match estimate) +const job = await xquikFetch("/extractions", { + method: "POST", + body: JSON.stringify({ + toolType: "follower_explorer", + targetUsername: "elonmusk", + resultsLimit: 1000, + }), +}); +// Response: { id: "77777", toolType: "follower_explorer", status: "completed", totalResults: 195000 } + +// Step 3: Poll until complete (large jobs may return status "running") +while (job.status === "pending" || job.status === "running") { + await new Promise((r) => setTimeout(r, 2000)); + job = await xquikFetch(`/extractions/${job.id}`); +} + +// Step 4: Retrieve paginated results (up to 1,000 per page) +let cursor; +const allResults = []; + +while (true) { + const path = `/extractions/${job.id}${cursor ? `?after=${cursor}` : ""}`; + const page = await xquikFetch(path); + allResults.push(...page.results); + // Each result: { xUserId, xUsername, xDisplayName, xFollowersCount, xVerified, xProfileImageUrl } + + if (!page.hasMore) break; + cursor = page.nextCursor; +} + +// Step 5: Export as CSV/XLSX/Markdown (50,000 row limit) +const exportUrl = `${BASE}/extractions/${job.id}/export?format=csv`; +const csvResponse = await fetch(exportUrl, { headers }); +const csvData = await csvResponse.text(); +``` + +## Giveaway Draws + +Run transparent, auditable giveaway draws from tweet replies with configurable filters. + +### Create Draw Request + +`POST /draws` with a `tweetUrl` (required) and optional filters: + +| Field | Type | Description | +|-------|------|-------------| +| `tweetUrl` | string | **Required.** Full tweet URL: `https://x.com/user/status/ID` | +| `winnerCount` | number | Winners to select (default 1) | +| `backupCount` | number | Backup winners to select | +| `uniqueAuthorsOnly` | boolean | Count only one entry per author | +| `mustRetweet` | boolean | Require participants to have retweeted | +| `mustFollowUsername` | string | Username participants must follow | +| `filterMinFollowers` | number | Minimum follower count | +| `filterAccountAgeDays` | number | Minimum account age in days | +| `filterLanguage` | string | Language code (e.g., `"en"`) | +| `requiredKeywords` | string[] | Words that must appear in the reply | +| `requiredHashtags` | string[] | Hashtags that must appear (e.g., `["#giveaway"]`) | +| `requiredMentions` | string[] | Usernames that must be mentioned (e.g., `["@xquik"]`) | + +### Complete Draw Workflow + +```javascript +// Step 1: Create draw with filters +const draw = await xquikFetch("/draws", { + method: "POST", + body: JSON.stringify({ + tweetUrl: "https://x.com/burakbayir/status/1893456789012345678", + winnerCount: 3, + backupCount: 2, + uniqueAuthorsOnly: true, + mustRetweet: true, + mustFollowUsername: "burakbayir", + filterMinFollowers: 50, + filterAccountAgeDays: 30, + filterLanguage: "en", + requiredHashtags: ["#giveaway"], + }), +}); + +// Step 2: Get draw details with winners +const details = await xquikFetch(`/draws/${draw.id}`); +// details.winners: [ +// { position: 1, authorUsername: "winner1", tweetId: "...", isBackup: false }, +// ... +// ] + +// Step 3: Export results +const exportUrl = `${BASE}/draws/${draw.id}/export?format=csv`; +``` + +## Webhook Event Handling + +Webhooks deliver events to your HTTPS endpoint with HMAC-SHA256 signatures. Each delivery is a POST with `X-Xquik-Signature` header and JSON body containing `eventType`, `username`, and `data`. + +### Webhook Handler (Express) + +```javascript +import express from "express"; +import { createHmac, timingSafeEqual, createHash } from "node:crypto"; + +const WEBHOOK_SECRET = process.env.XQUIK_WEBHOOK_SECRET; +const processedHashes = new Set(); // Use Redis/DB in production + +function verifySignature(payload, signature, secret) { + const expected = "sha256=" + createHmac("sha256", secret).update(payload).digest("hex"); + return timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); +} + +const app = express(); + +app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { + const signature = req.headers["x-xquik-signature"]; + const payload = req.body.toString(); + + // 1. Verify HMAC signature (constant-time comparison) + if (!signature || !verifySignature(payload, signature, WEBHOOK_SECRET)) { + return res.status(401).send("Invalid signature"); + } + + // 2. Deduplicate (retries can deliver the same event twice) + const payloadHash = createHash("sha256").update(payload).digest("hex"); + if (processedHashes.has(payloadHash)) { + return res.status(200).send("Already processed"); + } + processedHashes.add(payloadHash); + + // 3. Parse and route by event type + const event = JSON.parse(payload); + // event.eventType: "tweet.new" | "tweet.reply" | "tweet.quote" | "tweet.retweet" | "follower.gained" | "follower.lost" + + // 4. Respond within 10 seconds (process async if slow) + res.status(200).send("OK"); +}); + +app.listen(3000); +``` + +For Flask (Python) webhook handler, see [references/python-examples.md](references/python-examples.md#webhook-handler-flask). + +Webhook security rules: +- Always verify signature before processing (constant-time comparison) +- Compute HMAC over raw body bytes, not re-serialized JSON +- Respond `200` within 10 seconds; queue slow processing for async +- Deduplicate by payload hash (retries can deliver same event twice) +- Store webhook secret in environment variables, never hardcode +- Retry policy: 5 attempts with exponential backoff on failure + +Check delivery status via `GET /webhooks/{id}/deliveries` to monitor successful and failed attempts. + +## Real-Time Monitoring Setup + +Complete end-to-end: create monitor, register webhook, handle events. + +```javascript +// 1. Create monitor (free) +const monitor = await xquikFetch("/monitors", { + method: "POST", + body: JSON.stringify({ + username: "elonmusk", + eventTypes: ["tweet.new", "tweet.reply", "tweet.quote", "follower.gained"], + }), +}); + +// 2. Register webhook (free) +const webhook = await xquikFetch("/webhooks", { + method: "POST", + body: JSON.stringify({ + url: "https://your-server.com/webhook", + eventTypes: ["tweet.new", "tweet.reply"], + }), +}); +// IMPORTANT: Save webhook.secret. It is shown only once! + +// 3. Poll events (alternative to webhooks, free) +const events = await xquikFetch("/events?monitorId=7&limit=50"); +``` + +Event types: `tweet.new`, `tweet.quote`, `tweet.reply`, `tweet.retweet`, `follower.gained`, `follower.lost`. + +## MCP Server (AI Agents) + +The MCP server at `https://xquik.com/mcp` provides 2 tools. StreamableHTTP transport. API key auth (`x-api-key` header) for CLI/IDE clients; OAuth 2.1 for web clients (Claude.ai, ChatGPT Developer Mode). + +### Tools + +| Tool | Description | Cost | +|------|-------------|------| +| `explore` | Search the API endpoint catalog (read-only, no network calls) | Free | +| `xquik` | Execute API calls against your account (120 endpoints, 12 categories) | Varies | + +Supported platforms: Claude.ai, Claude Desktop, Claude Code, ChatGPT (Custom GPT, Agents SDK, Developer Mode), Codex CLI, Cursor, VS Code, Windsurf, OpenCode. + +For setup configs per platform, read [references/mcp-setup.md](references/mcp-setup.md). For tool details with selection rules, common mistakes, and unsupported operations, read [references/mcp-tools.md](references/mcp-tools.md). + +### MCP vs REST API + +| | MCP Server | REST API | +|---|------------|----------| +| **Best for** | AI agents, IDE integrations | Custom apps, scripts, backend services | +| **Model** | 2 tools (explore + xquik) | 120 individual endpoints | +| **Categories** | 12: account, automations, bot, composition, credits, extraction, integrations, media, monitoring, support, twitter, x-accounts, x-write | Same | +| **Coverage** | Full — `xquik` tool calls any REST endpoint | Direct HTTP calls | +| **File export** | Not available | CSV, XLSX, Markdown | +| **Unique to REST** | - | API key management, file export (CSV/XLSX/MD), account locale update | + +### Workflow Patterns + +Common multi-step sequences (all via `xquik` tool calling REST endpoints): + +- **Set up real-time alerts:** `POST /monitors` → `POST /webhooks` → `POST /webhooks/{id}/test` +- **Run a giveaway:** `GET /account` (check budget) → `POST /draws` +- **Bulk extraction:** `POST /extractions/estimate` → `POST /extractions` → `GET /extractions/{id}` +- **Full tweet analysis:** `GET /x/tweets/{id}` (metrics) → `POST /extractions` with `thread_extractor` +- **Find and analyze user:** `GET /x/users/{username}` → `GET /x/users/{id}/tweets` → `GET /x/tweets/{id}` +- **Compose algorithm-optimized tweet:** `POST /compose` (step=compose) → AI asks follow-ups → (step=refine) → AI drafts → (step=score) → iterate +- **Analyze tweet style:** `POST /styles` (fetch & cache) → `GET /styles/{username}` (reference) → `POST /compose` with `styleUsername` +- **Compare styles:** `POST /styles` for both accounts → `GET /styles/compare` +- **Track tweet performance:** `POST /styles` (cache tweets) → `GET /styles/{username}/performance` (live metrics) +- **Save & manage drafts:** `POST /compose` → `POST /drafts` → `GET /drafts` → `DELETE /drafts/{id}` +- **Download & share media:** `POST /x/media/download` (returns permanent hosted URLs) +- **Get trending news:** `GET /radar` (7 sources, free) → `POST /compose` with trending topic +- **Subscribe or manage billing:** `POST /subscribe` (returns Stripe URL) +- **Post a tweet:** `POST /x/accounts` (connect) → `POST /x/tweets` with `account` + `text` (optionally `POST /x/media` first) +- **Engage with tweets:** `POST /x/tweets/{id}/like`, `POST /x/tweets/{id}/retweet`, `POST /x/users/{id}/follow` +- **Set up Telegram alerts:** `POST /integrations` (type=telegram, chatId, eventTypes) → `POST /integrations/{id}/test` +- **Create automation flow:** `POST /automations` (name, triggerType, triggerConfig) → `POST /automations/{slug}/steps` (add actions) → `PATCH /automations/{slug}` (activate) +- **Check & top up credits:** `GET /credits` → `POST /credits/topup` +- **Open support ticket:** `POST /support/tickets` (subject, body) → `GET /support/tickets/{id}` (check status) → `POST /support/tickets/{id}/messages` (reply) + +## Conventions + +- **IDs are strings.** Bigint values; treat as opaque strings, never parse as numbers +- **Timestamps are ISO 8601 UTC.** Example: `2026-02-24T10:30:00.000Z` +- **Errors return JSON.** Format: `{ "error": "error_code" }` +- **Cursors are opaque.** Pass `nextCursor` as the `after` query parameter, never decode +- Export formats: `csv`, `xlsx`, `md` via `GET /extractions/{id}/export?format=csv` or `GET /draws/{id}/export?format=csv&type=winners` + +## Reference Files + +For additional detail beyond this guide: + +- **`references/mcp-tools.md`**: MCP tool selection rules, workflow patterns, common mistakes, and unsupported operations +- **`references/api-endpoints.md`**: All REST API endpoints with methods, paths, parameters, and response shapes +- **`references/python-examples.md`**: Python equivalents of all JavaScript examples (retry, extraction, draw, webhook) +- **`references/webhooks.md`**: Extended webhook examples, local testing with ngrok, delivery status monitoring +- **`references/mcp-setup.md`**: MCP server configuration for 10 IDEs and AI agent platforms +- **`references/extractions.md`**: Extraction tool details, export columns +- **`references/types.md`**: TypeScript type definitions for all REST API and MCP output objects diff --git a/skills/.curated/x-twitter-scraper/references/api-endpoints.md b/skills/.curated/x-twitter-scraper/references/api-endpoints.md new file mode 100644 index 00000000..ca90a39c --- /dev/null +++ b/skills/.curated/x-twitter-scraper/references/api-endpoints.md @@ -0,0 +1,1595 @@ +# Xquik REST API Endpoints + +Base URL: `https://xquik.com/api/v1` + +All requests require the `x-api-key` header. All responses are JSON. HTTPS only. + +## Table of Contents + +- [Account](#account) +- [API Keys](#api-keys) +- [Monitors](#monitors) +- [Events](#events) +- [Webhooks](#webhooks) +- [Draws](#draws) +- [Extractions](#extractions) +- [X API (Direct Lookups)](#x-api-direct-lookups) +- [X Media (Download)](#x-media-download) +- [Trends](#trends) +- [Radar](#radar) +- [Compose](#compose) +- [Drafts](#drafts) +- [Tweet Style Cache](#tweet-style-cache) +- [Account Identity](#account-identity) +- [Subscribe](#subscribe) +- [X Accounts (Connected)](#x-accounts-connected) +- [X Write](#x-write) +- [Integrations](#integrations) +- [Automations](#automations) +- [Credits](#credits) +- [Support](#support) + +--- + +## Account + +### Get Account + +``` +GET /account +``` + +Returns subscription status, monitor allocation, and current period usage. + +**Response:** +```json +{ + "plan": "active", + "monitorsAllowed": 1, + "monitorsUsed": 0, + "currentPeriod": { + "start": "2026-02-01T00:00:00.000Z", + "end": "2026-03-01T00:00:00.000Z", + "usagePercent": 45 + } +} +``` + +### Update Account + +``` +PATCH /account +``` + +Update account locale. Session auth only (not API key). + +**Body:** `{ "locale": "en" | "tr" | "es" }` + +--- + +## API Keys + +Session auth only. These endpoints do not accept API key auth. + +### Create API Key + +``` +POST /api-keys +``` + +**Body:** `{ "name": "My Key" }` (optional) + +**Response:** Returns `fullKey` (shown only once), `prefix`, `name`, `id`, `createdAt`. + +### List API Keys + +``` +GET /api-keys +``` + +Returns all keys with `id`, `name`, `prefix`, `isActive`, `createdAt`, `lastUsedAt`. Full key is never exposed. + +### Revoke API Key + +``` +DELETE /api-keys/{id} +``` + +Permanent and irreversible. The key stops working immediately. + +--- + +## Monitors + +### Create Monitor + +``` +POST /monitors +``` + +**Body:** +```json +{ + "username": "elonmusk", + "eventTypes": ["tweet.new", "tweet.reply", "tweet.quote"] +} +``` + +**Response:** +```json +{ + "id": "7", + "username": "elonmusk", + "xUserId": "44196397", + "eventTypes": ["tweet.new", "tweet.reply", "tweet.quote"], + "createdAt": "2026-02-24T10:30:00.000Z" +} +``` + +Event types: `tweet.new`, `tweet.quote`, `tweet.reply`, `tweet.retweet`, `follower.gained`, `follower.lost`. + +Returns `409 monitor_already_exists` if the username is already monitored. + +### List Monitors + +``` +GET /monitors +``` + +Returns all monitors (up to 200, no pagination). Response includes `monitors` array and `total` count. + +### Get Monitor + +``` +GET /monitors/{id} +``` + +### Update Monitor + +``` +PATCH /monitors/{id} +``` + +**Body:** `{ "eventTypes": [...], "isActive": true|false }` (both optional) + +### Delete Monitor + +``` +DELETE /monitors/{id} +``` + +Stops tracking and deletes all associated data. + +--- + +## Events + +### List Events + +``` +GET /events +``` + +**Query parameters:** + +| Param | Type | Description | +|-------|------|-------------| +| `monitorId` | string | Filter by monitor ID | +| `eventType` | string | Filter by event type | +| `limit` | number | Results per page (1-100, default 50) | +| `after` | string | Cursor for next page | + +**Response:** +```json +{ + "events": [ + { + "id": "9010", + "type": "tweet.new", + "monitorId": "7", + "username": "elonmusk", + "occurredAt": "2026-02-24T16:45:00.000Z", + "data": { + "tweetId": "1893556789012345678", + "text": "Hello world", + "metrics": { "likes": 3200, "retweets": 890, "replies": 245 } + } + } + ], + "hasMore": true, + "nextCursor": "MjAyNi0wMi0yNFQxNjozMDowMC4wMDBa..." +} +``` + +### Get Event + +``` +GET /events/{id} +``` + +Returns a single event with full details. + +--- + +## Webhooks + +### Create Webhook + +``` +POST /webhooks +``` + +**Body:** +```json +{ + "url": "https://your-server.com/webhook", + "eventTypes": ["tweet.new", "tweet.reply"] +} +``` + +**Response** includes a `secret` field (shown only once). Store it for signature verification. + +### List Webhooks + +``` +GET /webhooks +``` + +Returns all webhooks (up to 200). Secret is never exposed in list responses. + +### Update Webhook + +``` +PATCH /webhooks/{id} +``` + +**Body:** `{ "url": "...", "eventTypes": [...], "isActive": true|false }` (all optional) + +### Delete Webhook + +``` +DELETE /webhooks/{id} +``` + +Permanently removes the webhook. All future deliveries are stopped. + +### Test Webhook + +``` +POST /webhooks/{id}/test +``` + +Sends a `webhook.test` event to the webhook endpoint, HMAC-signed with the webhook's secret. Returns success or failure status with HTTP response details. + +**Payload delivered to your endpoint:** +```json +{ + "eventType": "webhook.test", + "data": { + "message": "Test delivery from Xquik" + }, + "timestamp": "2026-02-27T12:00:00.000Z" +} +``` + +The delivery includes the `X-Xquik-Signature` header, identical to production deliveries. + +Returns `400 webhook_inactive` if the webhook is disabled. Reactivate via `PATCH /webhooks/{id}` before testing. + +### List Deliveries + +``` +GET /webhooks/{id}/deliveries +``` + +View delivery attempts and statuses for a webhook. Statuses: `pending`, `delivered`, `failed`, `exhausted`. + +--- + +## Draws + +### Create Draw + +``` +POST /draws +``` + +Run a giveaway draw from a tweet. Picks random winners from replies. + +**Body:** +```json +{ + "tweetUrl": "https://x.com/user/status/1893456789012345678", + "winnerCount": 3, + "backupCount": 2, + "uniqueAuthorsOnly": true, + "mustRetweet": true, + "mustFollowUsername": "burakbayir", + "filterMinFollowers": 100, + "filterAccountAgeDays": 30, + "filterLanguage": "en", + "requiredKeywords": ["giveaway"], + "requiredHashtags": ["#contest"], + "requiredMentions": ["@xquik"] +} +``` + +All filter fields are optional. Only `tweetUrl` is required. + +**Response:** +```json +{ + "id": "42", + "tweetId": "1893456789012345678", + "tweetUrl": "https://x.com/user/status/1893456789012345678", + "tweetText": "Like & RT to enter! Picking 3 winners tomorrow.", + "tweetAuthorUsername": "xquik", + "tweetLikeCount": 4200, + "tweetRetweetCount": 1800, + "tweetReplyCount": 1500, + "tweetQuoteCount": 120, + "status": "completed", + "totalEntries": 1500, + "validEntries": 890, + "createdAt": "2026-02-24T10:00:00.000Z", + "drawnAt": "2026-02-24T10:01:00.000Z" +} +``` + +### List Draws + +``` +GET /draws +``` + +Cursor-paginated. Returns compact draw objects. + +### Get Draw + +``` +GET /draws/{id} +``` + +Returns full draw details including winners. + +### Export Draw + +``` +GET /draws/{id}/export?format=csv&type=winners +``` + +Formats: `csv`, `xlsx`, `md`. Types: `winners` (default), `entries`. Entry exports capped at 50,000 rows. + +--- + +## Extractions + +### Create Extraction + +``` +POST /extractions +``` + +Run a bulk data extraction job. See `references/extractions.md` for all 23 tool types. + +**Body:** +```json +{ + "toolType": "reply_extractor", + "targetTweetId": "1893704267862470862", + "resultsLimit": 500 +} +``` + +`resultsLimit` (optional): Maximum results to extract. Stops early instead of fetching all data. Useful for controlling costs. + +**Tweet Search Filters** (`tweet_search_extractor` only): + +| Field | Type | Description | +|-------|------|-------------| +| `fromUser` | string | Author username | +| `toUser` | string | Directed to user | +| `mentioning` | string | Mentions user | +| `language` | string | Language code (e.g., `en`) | +| `sinceDate` | string | Start date (YYYY-MM-DD) | +| `untilDate` | string | End date (YYYY-MM-DD) | +| `mediaType` | string | `images`, `videos`, `gifs`, or `media` | +| `minFaves` | number | Minimum likes | +| `minRetweets` | number | Minimum retweets | +| `minReplies` | number | Minimum replies | +| `verifiedOnly` | boolean | Verified authors only | +| `replies` | string | `include`, `exclude`, or `only` | +| `retweets` | string | `include`, `exclude`, or `only` | +| `exactPhrase` | string | Exact match text | +| `excludeWords` | string | Comma-separated words to exclude | +| `advancedQuery` | string | Raw X search operators appended to query | + +These filters are converted to X search operators and combined with `searchQuery`. + +**Response:** +```json +{ + "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "toolType": "reply_extractor", + "status": "running" +} +``` + +### Estimate Extraction + +``` +POST /extractions/estimate +``` + +Preview the cost before running. Same body as create. + +**Response:** +```json +{ + "allowed": true, + "source": "replyCount", + "estimatedResults": 150, + "usagePercent": 45, + "projectedPercent": 48 +} +``` + +### List Extractions + +``` +GET /extractions +``` + +Cursor-paginated. Filter by `status` and `toolType`. + +### Get Extraction + +``` +GET /extractions/{id} +``` + +Returns job details with paginated results (up to 1,000 per page). + +### Export Extraction + +``` +GET /extractions/{id}/export?format=csv +``` + +Formats: `csv`, `xlsx`, `md`. 50,000 row limit. Exports include enrichment columns not in the API response. + +--- + +## X API (Direct Lookups) + +Metered operations that count toward the monthly quota. + +### Get Tweet + +``` +GET /x/tweets/{id} +``` + +Returns full tweet with engagement metrics (likes, retweets, replies, quotes, views, bookmarks), author info (username, followers, verified status, profile picture), and optional attached media (photos/videos with URLs). + +### Get Article + +``` +GET /x/articles/{id} +``` + +Retrieve the full content of an X Article (long-form post) by tweet ID. Returns title, body text with block-level formatting, cover image, inline images, and engagement metrics. Metered. + +**Response:** +```json +{ + "title": "Why AI Will Transform Everything", + "coverImage": "https://pbs.twimg.com/...", + "bodyHtml": "

The future of AI...

", + "likeCount": 5200, + "retweetCount": 890, + "replyCount": 245, + "viewCount": 150000, + "bookmarkCount": 1200, + "author": { + "id": "44196397", + "username": "elonmusk", + "name": "Elon Musk" + } +} +``` + +### Search Tweets + +``` +GET /x/tweets/search?q={query} +``` + +Search using X syntax: keywords, `#hashtags`, `from:user`, `to:user`, `"exact phrases"`, `OR`, `-exclude`. + +Returns tweet info with optional engagement metrics (likeCount, retweetCount, replyCount) and optional attached media. Some fields may be omitted if unavailable. + +### Get User + +``` +GET /x/users/{username} +``` + +Returns profile info. Fields `id`, `username`, `name` are always present. All other fields (`description`, `followers`, `following`, `verified`, `profilePicture`, `location`, `createdAt`, `statusesCount`) are optional and omitted when unavailable. + +### Check Follower + +``` +GET /x/followers/check?source={username}&target={username} +``` + +Returns `isFollowing` and `isFollowedBy` for both directions. + +### Get User Tweets + +``` +GET /x/users/{id}/tweets +``` + +Get a user's recent tweets by user ID. Metered (1 credit/tweet). + +### Get User Likes + +``` +GET /x/users/{id}/likes +``` + +Get tweets liked by a user. Metered (1 credit/result). + +### Get User Media + +``` +GET /x/users/{id}/media +``` + +Get a user's media tweets (tweets containing photos/videos). Metered (1 credit/result). + +### Get Tweet Favoriters + +``` +GET /x/tweets/{id}/favoriters +``` + +Get users who liked a tweet. Metered (1 credit/result). + +### Get Mutual Followers + +``` +GET /x/users/{id}/followers-you-know +``` + +Get mutual followers (followers you know). Metered (1 credit/result). + +### Get Bookmarks + +``` +GET /x/bookmarks +``` + +Get bookmarked tweets. Requires a connected X account. Metered (1 credit/result). + +### Get Bookmark Folders + +``` +GET /x/bookmarks/folders +``` + +Get bookmark folders. Requires a connected X account. Metered (1 credit). + +### Get Notifications + +``` +GET /x/notifications +``` + +Get notifications with type filter. Requires a connected X account. Metered (1 credit/result). + +### Get Home Timeline + +``` +GET /x/timeline +``` + +Get home timeline. Requires a connected X account. Metered (1 credit/result). + +--- + +## X Media (Download) + +### Download Media + +``` +POST /x/media/download +``` + +Download images, videos, and GIFs from tweets. Single or bulk (up to 50). Returns a shareable gallery URL. + +**Body:** Provide either `tweetInput` (single tweet) or `tweetIds` (bulk). Exactly 1 is required. + +| Field | Type | Description | +|-------|------|-------------| +| `tweetInput` | string | Tweet URL or numeric tweet ID for a single download. Accepts `x.com` and `twitter.com` URL formats | +| `tweetIds` | string[] | Array of tweet URLs or IDs for bulk download. Maximum 50 items. Returns a single combined gallery | + +**Response (single):** +```json +{ + "tweetId": "1893456789012345678", + "galleryUrl": "https://xquik.com/gallery/abc123", + "cacheHit": false +} +``` + +**Response (bulk):** +```json +{ + "galleryUrl": "https://xquik.com/gallery/def456", + "totalTweets": 3, + "totalMedia": 7 +} +``` + +First download is metered (counts toward monthly quota). Subsequent requests for the same tweet return cached URLs at no cost (`cacheHit: true`). All downloads are saved to the gallery at `https://xquik.com/gallery`. + +Returns `400 no_media` if the tweet has no downloadable media. Returns `400 too_many_tweets` if bulk array exceeds 50 items. + +--- + +## Trends + +### List Trends + +``` +GET /trends?woeid=1&count=30 +``` + +Metered. Subscription required. Cached, refreshes every 15 minutes. + +**WOEIDs:** 1 (Worldwide), 23424977 (US), 23424975 (UK), 23424969 (Turkey), 23424950 (Spain), 23424829 (Germany), 23424819 (France), 23424856 (Japan), 23424848 (India), 23424768 (Brazil), 23424775 (Canada), 23424900 (Mexico). + +**Response:** +```json +{ + "trends": [ + { "name": "#AI", "description": "...", "rank": 1, "query": "#AI" } + ], + "total": 30, + "woeid": 1 +} +``` + +--- + +## Radar + +### List Radar Items + +``` +GET /radar +``` + +Get trending topics and news from 7 sources: Google Trends, Hacker News, Polymarket, TrustMRR, Wikipedia, GitHub Trending, Reddit. Free. + +**Query parameters:** + +| Param | Type | Description | +|-------|------|-------------| +| `source` | string | Filter by source: `google_trends`, `hacker_news`, `polymarket`, `trustmrr`, `wikipedia`, `github`, `reddit` | +| `category` | string | Filter by category: `general`, `tech`, `dev`, `science`, `culture`, `politics`, `business`, `entertainment` | +| `limit` | number | Items per page (1-100, default 50) | +| `hours` | number | Look-back window in hours (1-72, default 6) | +| `region` | string | Region code: `US`, `GB`, `TR`, `ES`, `DE`, `FR`, `JP`, `IN`, `BR`, `CA`, `MX`, `global` (default) | + +**Response:** +```json +{ + "items": [ + { + "id": "12345", + "title": "Claude 4.6 Released", + "description": "Anthropic releases Claude 4.6...", + "url": "https://example.com/article", + "imageUrl": "https://example.com/image.png", + "source": "hacker_news", + "sourceId": "hn_12345", + "category": "tech", + "region": "global", + "language": "en", + "score": 450, + "metadata": { "points": 450, "numberComments": 132, "author": "pgdev" }, + "publishedAt": "2026-03-05T10:00:00.000Z", + "createdAt": "2026-03-05T10:05:00.000Z" + } + ], + "hasMore": true, + "nextCursor": "NDUwfDIwMjYtMDMtMDRUMDg6MzA6MDAuMDAwWnwxMjM0NQ==" +} +``` + +Fields: `id`, `title`, `description?`, `url?`, `imageUrl?`, `source`, `sourceId`, `category`, `region`, `language`, `score`, `metadata`, `publishedAt`, `createdAt`. Response includes `hasMore` and `nextCursor` for pagination. + +--- + +## Compose + +### Compose Tweet + +``` +POST /compose +``` + +Compose, refine, and score tweets using X algorithm data. Free, 3-step workflow. + +**Body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `step` | string | Yes | `compose`, `refine`, or `score` | +| `topic` | string | No | Tweet topic (compose, refine) | +| `goal` | string | No | `engagement`, `followers`, `authority`, `conversation` | +| `styleUsername` | string | No | Cached style username for voice matching (compose) | +| `tone` | string | No | Desired tone (refine) | +| `additionalContext` | string | No | Extra context or URLs (refine) | +| `callToAction` | string | No | Desired CTA (refine) | +| `mediaType` | string | No | `photo`, `video`, `none` (refine) | +| `draft` | string | No | Tweet text to evaluate (score) | +| `hasLink` | boolean | No | Link attached (score) | +| `hasMedia` | boolean | No | Media attached (score) | + +**Response (step=compose):** Returns `contentRules`, `scorerWeights`, `followUpQuestions`, `algorithmInsights`, `engagementMultipliers`, `topPenalties`. + +**Response (step=refine):** Returns `compositionGuidance`, `examplePatterns`. + +**Response (step=score):** Returns `totalChecks`, `passedCount`, `topSuggestion`, `checklist[]` with `factor`, `passed`, `suggestion`. + +--- + +## Drafts + +### Create Draft + +`POST /drafts` + +Save a tweet draft for later. + +**Request body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `text` | string | Yes | The draft tweet text | +| `topic` | string | No | Topic the tweet is about | +| `goal` | string | No | Optimization goal: `engagement`, `followers`, `authority`, `conversation` | + +**Response (201):** + +```json +{ + "id": "123", + "text": "draft text", + "topic": "product launch", + "goal": "engagement", + "createdAt": "2026-02-24T10:30:00.000Z", + "updatedAt": "2026-02-24T10:30:00.000Z" +} +``` + +--- + +### List Drafts + +`GET /drafts` + +List saved tweet drafts with cursor pagination. + +**Query parameters:** + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `limit` | number | No | 50 | Results per page (max 50) | +| `afterCursor` | string | No | - | Pagination cursor from previous response | + +**Response (200):** + +```json +{ + "drafts": [ + { + "id": "123", + "text": "draft text", + "topic": "product launch", + "goal": "engagement", + "createdAt": "2026-02-24T10:30:00.000Z", + "updatedAt": "2026-02-24T10:30:00.000Z" + } + ], + "afterCursor": "cursor_string", + "hasMore": true +} +``` + +--- + +### Get Draft + +`GET /drafts/{id}` + +Get a specific draft by ID. + +**Response (200):** Single draft object. + +**Errors:** `400 invalid_id`, `404 draft_not_found` + +--- + +### Delete Draft + +`DELETE /drafts/{id}` + +Delete a draft. Returns `204 No Content`. + +**Errors:** `400 invalid_id`, `404 draft_not_found` + +--- + +## Tweet Style Cache + +### Analyze & Cache Style + +`POST /styles` + +Fetch recent tweets from an X account and cache them for style analysis. **Consumes API usage credits.** + +**Request body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `username` | string | Yes | X username to analyze (without @) | + +**Response (201):** + +```json +{ + "xUsername": "elonmusk", + "tweetCount": 20, + "isOwnAccount": false, + "fetchedAt": "2026-02-24T10:30:00.000Z", + "tweets": [ + { + "id": "1893456789012345678", + "text": "The future is now.", + "authorUsername": "elonmusk", + "createdAt": "2026-02-24T14:22:00.000Z" + } + ] +} +``` + +--- + +### List Cached Styles + +`GET /styles` + +List all cached tweet style profiles. Max 200 results, ordered by fetch date. + +**Response (200):** + +```json +{ + "styles": [ + { + "xUsername": "elonmusk", + "tweetCount": 20, + "isOwnAccount": false, + "fetchedAt": "2026-02-24T10:30:00.000Z" + } + ] +} +``` + +--- + +### Save Custom Style + +`PUT /styles/{username}` + +Save a custom style profile from tweet texts. Free, no usage cost. Replaces existing style if one exists with the same label. + +**Body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `label` | string | Yes | Style label name (1-30 characters) | +| `tweets` | object[] | Yes | Array of tweet objects (1-100). Each must have a `text` field | + +**Response (200):** Style object with label, `tweetCount`, `isOwnAccount: false`, `fetchedAt`, and `tweets` array. + +**Errors:** `400 invalid_input` + +--- + +### Get Cached Style + +`GET /styles/{username}` + +Get a cached style profile with full tweet data. + +**Response (200):** Full style object with `tweets` array. + +**Errors:** `404 style_not_found` + +--- + +### Delete Cached Style + +`DELETE /styles/{username}` + +Delete a cached style. Returns `204 No Content`. + +**Errors:** `404 style_not_found` + +--- + +### Compare Styles + +`GET /styles/compare?username1=A&username2=B` + +Compare two cached tweet style profiles side by side. + +**Query parameters:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `username1` | string | Yes | First X username | +| `username2` | string | Yes | Second X username | + +**Response (200):** + +```json +{ + "style1": { "xUsername": "user1", "tweetCount": 20, "isOwnAccount": true, "fetchedAt": "...", "tweets": [...] }, + "style2": { "xUsername": "user2", "tweetCount": 15, "isOwnAccount": false, "fetchedAt": "...", "tweets": [...] } +} +``` + +**Errors:** `400 missing_params`, `404 style_not_found` + +--- + +### Analyze Performance + +`GET /styles/{username}/performance` + +Get live engagement metrics for cached tweets. **Consumes API usage credits.** + +**Response (200):** + +```json +{ + "xUsername": "elonmusk", + "tweetCount": 20, + "tweets": [ + { + "id": "1893456789012345678", + "text": "The future is now.", + "likeCount": 42000, + "retweetCount": 8500, + "replyCount": 3200, + "quoteCount": 1100, + "viewCount": 5000000, + "bookmarkCount": 2400 + } + ] +} +``` + +**Errors:** `404 style_not_found` + +--- + +## Account Identity + +### Set X Identity + +`PUT /account/x-identity` + +Link your X username to your Xquik account. Required for own-account detection in style analysis. + +**Request body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `username` | string | Yes | Your X username (without @) | + +**Response (200):** + +```json +{ + "success": true, + "xUsername": "elonmusk" +} +``` + +**Errors:** `400 invalid_input` + +--- + +## Subscribe + +### Get Subscription Link + +``` +POST /subscribe +``` + +Returns a Stripe Checkout URL for subscribing or managing the subscription. If already subscribed, returns the billing portal URL. + +**Response:** +```json +{ + "url": "https://checkout.stripe.com/c/pay/..." +} +``` + +--- + +## X Accounts (Connected) + +Manage connected X accounts for write actions. All endpoints are free (no usage cost). + +### List X Accounts + +``` +GET /x/accounts +``` + +Returns all connected X accounts. Response: `{ accounts: [{ id, username, displayName, isActive, createdAt }] }`. + +### Connect X Account + +``` +POST /x/accounts +``` + +**Body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `username` | string | Yes | X username (`@` auto-stripped) | +| `email` | string | Yes | Email associated with the X account | +| `password` | string | Yes | Account password (encrypted at rest) | +| `totp_secret` | string | No | TOTP base32 secret for 2FA accounts | +| `proxy_country` | string | No | Preferred proxy region (e.g. `"US"`, `"TR"`) | + +**Response (201):** `{ id, username, isActive, createdAt }` + +**Errors:** `409 account_already_connected`, `422 login_failed` + +### Get X Account + +``` +GET /x/accounts/{id} +``` + +Returns `{ id, username, displayName, isActive, createdAt }`. + +### Disconnect X Account + +``` +DELETE /x/accounts/{id} +``` + +Permanently removes the account and deletes stored credentials. Returns `{ success: true }`. + +### Re-authenticate X Account + +``` +POST /x/accounts/{id}/reauth +``` + +Use when a session expires or X requires re-verification. + +**Body:** `{ "password": "...", "totp_secret": "..." }` (password required, totp_secret optional) + +**Response:** `{ success: true }` + +**Errors:** `422 reauth_failed` + +--- + +## X Write + +Write actions performed through connected X accounts. All endpoints are metered. Every request requires an `account` field (username or account ID) identifying which connected account to use. + +### Create Tweet + +``` +POST /x/tweets +``` + +**Body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `account` | string | Yes | Connected X username or account ID | +| `text` | string | Yes | Tweet text (280 chars, or 25,000 if `is_note_tweet` is true) | +| `reply_to_tweet_id` | string | No | Tweet ID to reply to | +| `attachment_url` | string | No | URL to attach as a card | +| `community_id` | string | No | Community ID to post into | +| `is_note_tweet` | boolean | No | Long-form note tweet (up to 25,000 chars) | +| `media_ids` | string[] | No | Media IDs from `POST /x/media` (max 4 images or 1 video) | + +**Response:** `{ tweetId, success: true }` + +**Errors:** `502 x_write_failed` + +### Delete Tweet + +``` +DELETE /x/tweets/{id} +``` + +**Body:** `{ "account": "username" }` + +**Response:** `{ success: true }` + +### Like Tweet + +``` +POST /x/tweets/{id}/like +``` + +**Body:** `{ "account": "username" }` + +### Unlike Tweet + +``` +DELETE /x/tweets/{id}/like +``` + +**Body:** `{ "account": "username" }` + +### Retweet + +``` +POST /x/tweets/{id}/retweet +``` + +**Body:** `{ "account": "username" }` + +### Follow User + +``` +POST /x/users/{id}/follow +``` + +**Body:** `{ "account": "username" }` + +**Errors:** `502 x_write_failed` + +### Unfollow User + +``` +DELETE /x/users/{id}/follow +``` + +**Body:** `{ "account": "username" }` + +### Send DM + +``` +POST /x/dm/{userId} +``` + +**Body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `account` | string | Yes | Connected X username or account ID | +| `text` | string | Yes | Message text | +| `media_ids` | string[] | No | Media IDs to attach | +| `reply_to_message_id` | string | No | Message ID to reply to | + +### Get DM History + +``` +GET /x/dm/{userId}/history +``` + +Get DM conversation history with a user. Requires a connected X account. Metered (1 credit/result). + +### Update Profile + +``` +PATCH /x/profile +``` + +**Body:** `{ "account": "username", "name": "...", "description": "...", "location": "...", "url": "..." }` (account required, others optional) + +### Update Avatar + +``` +PATCH /x/profile/avatar +``` + +Update profile avatar. Max 700 KB, GIF/JPEG/PNG. Metered (2 credits). + +**Body:** FormData with `account` (required) and `file` (required, max 700 KB). + +### Update Banner + +``` +PATCH /x/profile/banner +``` + +Update profile banner. Max 2 MB, GIF/JPEG/PNG. Metered (2 credits). + +**Body:** FormData with `account` (required) and `file` (required, max 2 MB). + +### Upload Media + +``` +POST /x/media +``` + +**Body:** FormData with `account` (required), `file` (required), and `is_long_video` (optional boolean). Alternatively, JSON body with `account` (required) and `url` (required, direct media URL) for URL-based upload. + +**Response:** Returns a media ID to pass in `media_ids` when creating a tweet. + +### Create Community + +``` +POST /x/communities +``` + +**Body:** `{ "account": "username", "name": "...", "description": "..." }` (all required) + +### Delete Community + +``` +DELETE /x/communities/{id} +``` + +**Body:** `{ "account": "username", "community_name": "..." }` (name required for confirmation) + +### Join Community + +``` +POST /x/communities/{id}/join +``` + +**Body:** `{ "account": "username" }` + +**Errors:** `409 already_member` + +### Leave Community + +``` +DELETE /x/communities/{id}/join +``` + +**Body:** `{ "account": "username" }` + +--- + +## Integrations + +Manage third-party integrations (currently Telegram) that receive monitor event notifications. All endpoints are free (no usage cost). + +### Create Integration + +``` +POST /integrations +``` + +**Body:** + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | Yes | Integration type: `"telegram"` | +| `name` | string | Yes | Human-readable name | +| `config` | object | Yes | Type-specific config. Telegram: `{ chatId: "-1001234567890" }` | +| `eventTypes` | string[] | Yes | Event types: `tweet.new`, `tweet.quote`, `tweet.reply`, `tweet.retweet`, `draw.completed`, `extraction.completed`, `extraction.failed` | + +**Response (201):** `{ id, type, name, config, eventTypes, isActive, createdAt }` + +### List Integrations + +``` +GET /integrations +``` + +Returns all integrations. Response: `{ integrations: [...] }`. + +### Get Integration + +``` +GET /integrations/{id} +``` + +Returns a single integration with full details. + +### Update Integration + +``` +PATCH /integrations/{id} +``` + +**Body:** `{ "name": "...", "eventTypes": [...], "isActive": true|false, "silentPush": false, "scopeAllMonitors": true, "filters": {}, "messageTemplate": {} }` (all optional, at least 1 required) + +### Delete Integration + +``` +DELETE /integrations/{id} +``` + +Permanently removes the integration. Returns `204 No Content`. + +### Test Integration + +``` +POST /integrations/{id}/test +``` + +Sends a test notification. Returns success or failure status. + +**Errors:** `502 delivery_failed` + +### List Deliveries + +``` +GET /integrations/{id}/deliveries +``` + +View delivery attempts and statuses. Statuses: `pending`, `delivered`, `failed`, `exhausted`. + +**Query:** `limit` (default 50). + +--- + +## Automations + +Trigger-driven workflow automation. Create flows with triggers (monitor events, schedules, search, inbound webhooks) and action steps. + +### List Automations + +``` +GET /automations +``` + +Returns all flows. Response: `{ items: [{ id, name, slug, triggerType, triggerConfig, isActive, runCount, lastRunAt, minIntervalSeconds, pausedReason, templateSlug, xAccountId, createdAt, updatedAt }] }`. + +### Create Automation + +``` +POST /automations +``` + +**Body:** +```json +{ + "name": "New Follower Welcome", + "triggerType": "monitor_event", + "triggerConfig": { "eventType": "follower.gained" }, + "templateSlug": "welcome-dm" +} +``` + +Trigger types: `monitor_event`, `schedule`, `search`, `webhook_inbound`. + +**Response (201):** Flow object with `id`, `slug`, `isActive: false`. + +Flows are created inactive. Add steps, then activate via `PATCH /automations/{slug}`. + +Free: 2 flows. Subscribers: 10 flows. + +### Get Automation + +``` +GET /automations/{slug} +``` + +Returns flow with steps and 20 most recent runs. + +### Update Automation + +``` +PATCH /automations/{slug} +``` + +**Body:** `{ "expectedUpdatedAt": "...", "name": "...", "triggerType": "...", "triggerConfig": {...}, "isActive": true|false }`. `expectedUpdatedAt` required (optimistic concurrency). Returns `409 conflict` if stale. + +Activation requires an active subscription and at least 1 action step. + +### Delete Automation + +``` +DELETE /automations/{slug} +``` + +Deletes the flow and all its steps. Returns `{ success: true }`. + +### Add Step + +``` +POST /automations/{slug}/steps +``` + +**Body:** +```json +{ + "stepType": "action", + "actionType": "send_dm", + "branch": "main", + "config": { "message": "Welcome!" }, + "position": 0 +} +``` + +Step types: `action`, `condition`, `extraction`. Max 10 steps per flow. + +Action types: `create_tweet`, `follow`, `like`, `reply_tweet`, `retweet`, `send_dm`, `send_email`, `send_telegram`, `unfollow`. + +Extraction types: all 23 extraction tool types (kebab-case, e.g. `reply-extractor`). Requires `outputName` for variable reference in later steps. + +### Update Step + +``` +PATCH /automations/{slug}/steps +``` + +**Body:** `{ "stepId": "101", "config": {...}, "positionX": 250, "positionY": 100 }`. `stepId` required. + +### Delete Step + +``` +DELETE /automations/{slug}/steps +``` + +**Body:** `{ "stepId": "101" }`. + +### Update Step Positions + +``` +PATCH /automations/{slug}/steps/positions +``` + +Batch update canvas positions: `{ "positions": [{ "stepId": "101", "positionX": 250, "positionY": 100 }] }`. + +### Test Automation + +``` +POST /automations/{slug}/test +``` + +Not yet implemented. Returns `{ status: "not_implemented" }`. + +### Inbound Webhook Trigger + +``` +POST /webhooks/inbound/{token} +``` + +No auth header required. The URL token identifies the flow. Accepts any JSON body as trigger payload. Rate limited per flow (60/hour) and per user (300/hour). + +--- + +## Credits + +### Get Credit Balance + +``` +GET /credits +``` + +Get credit balance, lifetime purchased/used, and auto top-up status. Free. + +### Top Up Credits + +``` +POST /credits/topup +``` + +Get a Stripe checkout URL to purchase credits ($10 minimum). Free. + +--- + +## Support + +### Create Ticket + +``` +POST /support/tickets +``` + +**Body:** `{ "subject": "...", "body": "..." }` + +**Response (201):** `{ id, subject, status, createdAt }` + +### List Tickets + +``` +GET /support/tickets +``` + +Returns all tickets for the authenticated user. + +### Get Ticket + +``` +GET /support/tickets/{id} +``` + +Returns ticket with messages. + +### Update Ticket + +``` +PATCH /support/tickets/{id} +``` + +Update ticket status. + +### Reply to Ticket + +``` +POST /support/tickets/{id}/messages +``` + +**Body:** `{ "body": "..." }` + +Add a message to an existing ticket. + +--- + +## Error Codes + +| Status | Code | Meaning | +|--------|------|---------| +| 400 | `invalid_input` | Request body failed validation | +| 400 | `invalid_id` | Path parameter is not a valid ID | +| 400 | `invalid_json` | Invalid JSON in request body | +| 400 | `invalid_tweet_url` | Tweet URL format is invalid | +| 400 | `invalid_tweet_id` | Tweet ID is empty or invalid | +| 400 | `invalid_username` | X username is empty or invalid | +| 400 | `invalid_tool_type` | Extraction tool type not recognized | +| 400 | `invalid_format` | Export format not `csv`, `xlsx`, or `md` | +| 400 | `invalid_params` | Export query parameters are missing or invalid | +| 400 | `missing_query` | Required query parameter is missing | +| 400 | `missing_params` | Required query parameters are missing | +| 400 | `no_media` | Tweet has no downloadable media | +| 400 | `webhook_inactive` | Webhook is disabled (test-webhook only) | +| 401 | `unauthenticated` | Missing or invalid API key | +| 403 | `account_needs_reauth` | X account session expired, re-authenticate | +| 402 | `no_subscription` | No active subscription | +| 402 | `subscription_inactive` | Subscription is not active | +| 402 | `usage_limit_reached` | Monthly usage cap exceeded | +| 402 | `extra_usage_disabled` | Extra usage not enabled | +| 402 | `extra_usage_requires_v2` | Extra usage requires the new pricing plan | +| 402 | `frozen` | Extra usage paused, outstanding payment required | +| 402 | `overage_limit_reached` | Overage spending limit reached | +| 402 | `no_addon` | No monitor addon on subscription | +| 403 | `monitor_limit_reached` | Plan monitor limit exceeded | +| 403 | `api_key_limit_reached` | API key limit reached (100 max) | +| 403 | `flow_limit_reached` | Flow limit reached (free: 2, subscriber: 10) | +| 403 | `step_limit_reached` | Step limit reached (10 per flow) | +| 404 | `not_found` | Resource does not exist | +| 404 | `user_not_found` | X user not found | +| 404 | `tweet_not_found` | Tweet not found | +| 404 | `style_not_found` | No cached style found | +| 404 | `draft_not_found` | Draft not found | +| 409 | `monitor_already_exists` | Duplicate monitor for same username | +| 409 | `conflict` | Concurrent edit conflict (automation updates) | +| 422 | `login_failed` | X credential verification failed | +| 429 | - | Rate limited. Retry with backoff | +| 429 | `x_api_rate_limited` | X data source rate limited. Retry | +| 500 | `internal_error` | Server error | +| 502 | `stream_registration_failed` | Stream registration failed. Retry | +| 502 | `x_api_unavailable` | X data source temporarily unavailable | +| 502 | `x_api_unauthorized` | X data source authentication failed. Retry | +| 502 | `delivery_failed` | Integration test delivery failed | diff --git a/skills/.curated/x-twitter-scraper/references/extractions.md b/skills/.curated/x-twitter-scraper/references/extractions.md new file mode 100644 index 00000000..d9210407 --- /dev/null +++ b/skills/.curated/x-twitter-scraper/references/extractions.md @@ -0,0 +1,229 @@ +# Xquik Extraction Tools + +23 bulk data extraction tools. Each requires a specific target parameter. + +**Endpoint:** `POST /extractions` + +**Always estimate first:** `POST /extractions/estimate` with the same body to preview cost and check quota. + +## Tool Types + +### Tweet-Based (require `targetTweetId`) + +| Tool Type | Description | +|-----------|-------------| +| `reply_extractor` | Extract users who replied to a tweet | +| `repost_extractor` | Extract users who retweeted a tweet | +| `quote_extractor` | Extract users who quote-tweeted a tweet | +| `thread_extractor` | Extract all tweets in a thread | +| `article_extractor` | Extract article content linked in a tweet | +| `favoriters` | Extract users who favorited a tweet | + +**Example:** +```json +{ + "toolType": "reply_extractor", + "targetTweetId": "1893704267862470862" +} +``` + +### User-Based (require `targetUsername`) + +| Tool Type | Description | +|-----------|-------------| +| `follower_explorer` | Extract followers of an account | +| `following_explorer` | Extract accounts followed by a user | +| `verified_follower_explorer` | Extract verified followers of an account | +| `mention_extractor` | Extract tweets mentioning an account | +| `post_extractor` | Extract posts from an account | + +**Example:** +```json +{ + "toolType": "follower_explorer", + "targetUsername": "elonmusk" +} +``` + +The `@` prefix is automatically stripped if included. + +### User-Based by ID (require `targetUserId`) + +| Tool Type | Description | +|-----------|-------------| +| `user_likes` | Extract tweets liked by a user | +| `user_media` | Extract media tweets from a user | + +**Example:** +```json +{ + "toolType": "user_likes", + "targetUserId": "44196397" +} +``` + +### Community-Based (require `targetCommunityId`) + +| Tool Type | Description | +|-----------|-------------| +| `community_extractor` | Extract members of a community | +| `community_moderator_explorer` | Extract moderators of a community | +| `community_post_extractor` | Extract posts from a community | +| `community_search` | Search posts within a community (also requires `searchQuery`) | + +**Example:** +```json +{ + "toolType": "community_extractor", + "targetCommunityId": "1234567890" +} +``` + +### List-Based (require `targetListId`) + +| Tool Type | Description | +|-----------|-------------| +| `list_member_extractor` | Extract members of a list | +| `list_post_extractor` | Extract posts from a list | +| `list_follower_explorer` | Extract followers of a list | + +**Example:** +```json +{ + "toolType": "list_member_extractor", + "targetListId": "1234567890" +} +``` + +### Space-Based (require `targetSpaceId`) + +| Tool Type | Description | +|-----------|-------------| +| `space_explorer` | Extract participants of a Space | + +**Example:** +```json +{ + "toolType": "space_explorer", + "targetSpaceId": "1YqKDqDXAbwKV" +} +``` + +### Search-Based (require `searchQuery`) + +| Tool Type | Description | +|-----------|-------------| +| `people_search` | Search for users by keyword | +| `tweet_search_extractor` | Search and extract tweets by keyword or hashtag (bulk, up to 1,000) | + +**Example (people search):** +```json +{ + "toolType": "people_search", + "searchQuery": "machine learning engineer" +} +``` + +**Example (tweet search):** +```json +{ + "toolType": "tweet_search_extractor", + "searchQuery": "#AI", + "resultsLimit": 100 +} +``` + +### Tweet Search Filters + +The `tweet_search_extractor` tool type supports 16 additional filter fields that are converted to X search operators and combined with `searchQuery`: + +| Field | Type | Description | +|-------|------|-------------| +| `fromUser` | string | Author username | +| `toUser` | string | Directed to user | +| `mentioning` | string | Mentions user | +| `language` | string | Language code (e.g., `en`) | +| `sinceDate` | string | Start date (YYYY-MM-DD) | +| `untilDate` | string | End date (YYYY-MM-DD) | +| `mediaType` | string | `images`, `videos`, `gifs`, or `media` | +| `minFaves` | number | Minimum likes | +| `minRetweets` | number | Minimum retweets | +| `minReplies` | number | Minimum replies | +| `verifiedOnly` | boolean | Verified authors only | +| `replies` | string | `include`, `exclude`, or `only` | +| `retweets` | string | `include`, `exclude`, or `only` | +| `exactPhrase` | string | Exact match text | +| `excludeWords` | string | Comma-separated words to exclude | +| `advancedQuery` | string | Raw X search operators appended to query | + +**Example with filters:** +```json +{ + "toolType": "tweet_search_extractor", + "searchQuery": "AI", + "fromUser": "elonmusk", + "minFaves": 100, + "sinceDate": "2026-01-01", + "mediaType": "videos", + "resultsLimit": 500 +} +``` + +`resultsLimit` (optional): Maximum results to extract. Stops early instead of fetching all. Pass this on both `POST /extractions/estimate` and `POST /extractions` when you only need a specific count. + +## Response + +```json +{ + "id": "77777", + "toolType": "reply_extractor", + "status": "completed", + "totalResults": 150 +} +``` + +Statuses: `pending`, `running`, `completed`, `failed`. + +## Retrieving Results + +``` +GET /extractions/{id} +``` + +Returns paginated results (up to 1,000 per page). Each result includes: + +- `xUserId`, `xUsername`, `xDisplayName` +- `xFollowersCount`, `xVerified`, `xProfileImageUrl` +- `tweetId`, `tweetText`, `tweetCreatedAt` (for tweet-based extractions) + +## Exporting Results + +``` +GET /extractions/{id}/export?format=csv +``` + +Formats: `csv`, `xlsx`, `md`. 50,000 row limit. + +Exports include enrichment columns not present in the API response. + +## Estimating Cost + +``` +POST /extractions/estimate +``` + +Same body as create. Response: + +```json +{ + "allowed": true, + "source": "replyCount", + "estimatedResults": 150, + "usagePercent": 45, + "projectedPercent": 48 +} +``` + +If `allowed` is `false`, the extraction would exceed your monthly quota. + +For common mistakes and tool selection rules, see [mcp-tools.md](mcp-tools.md#common-mistakes). diff --git a/skills/.curated/x-twitter-scraper/references/mcp-setup.md b/skills/.curated/x-twitter-scraper/references/mcp-setup.md new file mode 100644 index 00000000..8be00b9d --- /dev/null +++ b/skills/.curated/x-twitter-scraper/references/mcp-setup.md @@ -0,0 +1,201 @@ +# Xquik MCP Server Setup + +Connect AI agents and IDEs to Xquik via the Model Context Protocol. The MCP server uses the same API key as the REST API. + +| Setting | Value | +|---------|-------| +| Protocol | HTTP (StreamableHTTP) | +| Endpoint | `https://xquik.com/mcp` | +| Auth header | `x-api-key` | + +## Claude.ai (Web) + +Claude.ai supports MCP connectors natively via OAuth. Add Xquik as a connector from **Settings > Feature Preview > Integrations > Add More > Xquik**. The OAuth 2.1 flow handles authentication automatically. No API key needed. + +## Claude Desktop + +Claude Desktop only supports stdio transport. Use `mcp-remote` as a bridge (requires [Node.js](https://nodejs.org)). + +Add to `claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "xquik": { + "command": "npx", + "args": [ + "mcp-remote@latest", + "https://xquik.com/mcp", + "--header", + "x-api-key:xq_YOUR_KEY_HERE" + ] + } + } +} +``` + +## Claude Code + +Add to `.mcp.json`: + +```json +{ + "mcpServers": { + "xquik": { + "type": "http", + "url": "https://xquik.com/mcp", + "headers": { + "x-api-key": "xq_YOUR_KEY_HERE" + } + } + } +} +``` + +## ChatGPT + +3 ways to connect ChatGPT to Xquik: + +### Option 1: Custom GPT (Recommended) + +Create a Custom GPT and add Xquik as an Action using the OpenAPI schema at `https://docs.xquik.com/openapi.json`. Set the API key under Authentication > API Key > Header `x-api-key`. + +### Option 2: Agents SDK + +Use the [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/mcp/) for programmatic access: + +```python +from agents.mcp import MCPServerStreamableHttp + +async with MCPServerStreamableHttp( + url="https://xquik.com/mcp", + headers={"x-api-key": "xq_YOUR_KEY_HERE"}, + params={}, +) as xquik: + # use xquik as a tool provider + pass +``` + +### Option 3: Developer Mode + +ChatGPT Developer Mode supports MCP connectors via OAuth. Add Xquik from **Settings > Developer Mode > MCP Tools > Add**. Enter `https://xquik.com/mcp` as the endpoint. OAuth handles authentication automatically. + +## Codex CLI + +Add to `~/.codex/config.toml`: + +```toml +[mcp_servers.xquik] +url = "https://xquik.com/mcp" +http_headers = { "x-api-key" = "xq_YOUR_KEY_HERE" } +``` + +## Cursor + +Add to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project): + +```json +{ + "mcpServers": { + "xquik": { + "url": "https://xquik.com/mcp", + "headers": { + "x-api-key": "xq_YOUR_KEY_HERE" + } + } + } +} +``` + +## VS Code + +Add to `.vscode/mcp.json` (project) or use **MCP: Open User Configuration** (global): + +```json +{ + "servers": { + "xquik": { + "type": "http", + "url": "https://xquik.com/mcp", + "headers": { + "x-api-key": "xq_YOUR_KEY_HERE" + } + } + } +} +``` + +## Windsurf + +Add to `~/.codeium/windsurf/mcp_config.json`: + +```json +{ + "mcpServers": { + "xquik": { + "serverUrl": "https://xquik.com/mcp", + "headers": { + "x-api-key": "xq_YOUR_KEY_HERE" + } + } + } +} +``` + +## OpenCode + +Add to `opencode.json`: + +```json +{ + "mcp": { + "xquik": { + "type": "remote", + "url": "https://xquik.com/mcp", + "headers": { + "x-api-key": "xq_YOUR_KEY_HERE" + } + } + } +} +``` + +## MCP Server Architecture + +The default MCP server (v2) at `https://xquik.com/mcp` uses a **code-execution sandbox model** with 2 tools: + +| Tool | Description | Cost | +|------|-------------|------| +| `explore` | Search the API endpoint catalog (read-only, no network calls) | Free | +| `xquik` | Execute API calls against your account | Varies by endpoint | + +The agent writes async JavaScript arrow functions that run in a sandboxed environment. Auth is injected automatically. The sandbox covers all 97 REST API endpoints across 12 categories: account, automations, bot, composition, extraction, integrations, media, monitoring, support, twitter, x-accounts, and x-write. + +## After Setup + +### Workflow Patterns + +| Workflow | Steps (via `xquik` tool) | +|----------|--------------------------| +| Set up real-time alerts | `POST /monitors` -> `POST /webhooks` -> `POST /webhooks/{id}/test` | +| Run a giveaway | `GET /account` -> `POST /draws` | +| Bulk extraction | `POST /extractions/estimate` -> `POST /extractions` -> `GET /extractions/{id}` | +| Compose optimized tweet | `POST /compose` (step=compose -> refine -> score) | +| Subscribe or manage billing | `POST /subscribe` | + +### Example Prompts + +Try these with your AI agent: + +- "Monitor @vercel for new tweets and quote tweets" +- "How many followers does @elonmusk have?" +- "Search for tweets mentioning xquik" +- "What does this tweet say? https://x.com/elonmusk/status/1893456789012345678" +- "Does @elonmusk follow @SpaceX back?" +- "Pick 3 winners from this tweet: https://x.com/burakbayir/status/1893456789012345678" +- "How much would it cost to extract all followers of @elonmusk?" +- "What's trending in the US right now?" +- "What's trending on Hacker News today?" +- "Help me write a tweet about launching my product" +- "Set up a webhook at https://my-server.com/events for new tweets" +- "What plan am I on and how much have I used?" diff --git a/skills/.curated/x-twitter-scraper/references/mcp-tools.md b/skills/.curated/x-twitter-scraper/references/mcp-tools.md new file mode 100644 index 00000000..646bd462 --- /dev/null +++ b/skills/.curated/x-twitter-scraper/references/mcp-tools.md @@ -0,0 +1,151 @@ +# Xquik MCP Tools Reference + +The MCP server at `https://xquik.com/mcp` uses a code-execution sandbox model with 2 tools. The agent writes async JavaScript arrow functions that run in a sandboxed environment with auth injected automatically. + +## Tools + +| Tool | Description | Cost | +|------|-------------|------| +| `explore` | Search the API endpoint catalog (read-only, no network calls) | Free | +| `xquik` | Execute API calls against your account | Varies by endpoint | + +### `explore` — Search the API Spec + +The sandbox provides an in-memory `spec.endpoints` array. Filter/search it to find endpoints before calling them. + +```typescript +interface EndpointInfo { + method: string; + path: string; + summary: string; + category: string; // account, automations, bot, composition, credits, extraction, integrations, 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: + +```javascript +// Find all free endpoints +async () => spec.endpoints.filter(e => e.free); + +// Find endpoints by category +async () => spec.endpoints.filter(e => e.category === 'x-write'); + +// Search by keyword +async () => spec.endpoints.filter(e => e.summary.toLowerCase().includes('tweet')); +``` + +### `xquik` — Execute API Calls + +The sandbox provides `xquik.request()` with auth injected automatically. Never pass API keys. + +```typescript +declare const xquik: { + request(path: string, options?: { + method?: string; // default: 'GET' + body?: unknown; + query?: Record; + }): Promise; +}; +declare const spec: { endpoints: EndpointInfo[] }; +``` + +## Tool Selection Rules + +Use `explore` first to find endpoints, then `xquik` to call them. + +| Goal | Endpoint (via `xquik`) | +|------|------------------------| +| Single tweet by ID or URL | `GET /api/v1/x/tweets/{id}` | +| Full X Article by tweet ID | `GET /api/v1/x/articles/{id}` | +| Search tweets by keyword/hashtag | `GET /api/v1/x/tweets/search?q=...` | +| User profile, bio, follower counts | `GET /api/v1/x/users/{username}` | +| Download media from tweets | `POST /api/v1/x/media/download` | +| Check follow relationship | `GET /api/v1/x/followers/check?source=A&target=B` | +| Trending topics by region (X) | `GET /api/v1/trends?woeid=1` | +| Trending news from 7 sources | `GET /api/v1/radar` (via `xquik` tool) | +| Activity from monitored accounts | `GET /api/v1/events` | +| Budget, plan, usage percent | `GET /api/v1/account` | +| Monitor an X account | `POST /api/v1/monitors` | +| Set up webhook notifications | `POST /api/v1/webhooks` | +| Run a giveaway draw | `POST /api/v1/draws` | +| Subscribe or manage billing | `POST /api/v1/subscribe` | +| Compose/draft a tweet | `POST /api/v1/compose` (3-step: compose, refine, score) | +| Link your X username | `PUT /api/v1/account/x-identity` | +| Analyze tweet style | `POST /api/v1/styles` | +| Get cached style | `GET /api/v1/styles/{username}` | +| Compare two styles | `GET /api/v1/styles/compare` | +| Post a tweet | `POST /api/v1/x/tweets` (requires connected account) | +| Like/unlike a tweet | `POST`/`DELETE /api/v1/x/tweets/{id}/like` | +| Retweet | `POST /api/v1/x/tweets/{id}/retweet` | +| Follow/unfollow | `POST`/`DELETE /api/v1/x/users/{id}/follow` | +| Send a DM | `POST /api/v1/x/dm/{userId}` | +| Upload media | `POST /api/v1/x/media` | +| Create automation flow | `POST /api/v1/automations` | +| List automation flows | `GET /api/v1/automations` | +| Add step to flow | `POST /api/v1/automations/{slug}/steps` | +| Activate/deactivate flow | `PATCH /api/v1/automations/{slug}` | +| Open support ticket | `POST /api/v1/support/tickets` | +| List support tickets | `GET /api/v1/support/tickets` | +| Get user's recent tweets | `GET /api/v1/x/users/{id}/tweets` | +| Get user's liked tweets | `GET /api/v1/x/users/{id}/likes` | +| Get user's media tweets | `GET /api/v1/x/users/{id}/media` | +| Get tweet favoriters (who liked) | `GET /api/v1/x/tweets/{id}/favoriters` | +| Get mutual followers | `GET /api/v1/x/users/{id}/followers-you-know` | +| Get bookmarks | `GET /api/v1/x/bookmarks` | +| Get bookmark folders | `GET /api/v1/x/bookmarks/folders` | +| Get notifications | `GET /api/v1/x/notifications` | +| Get home timeline | `GET /api/v1/x/timeline` | +| Get DM history | `GET /api/v1/x/dm/{userId}/history` | +| Check credit balance | `GET /api/v1/credits` | +| Top up credits | `POST /api/v1/credits/topup` | + +Use `POST /api/v1/extractions` ONLY for bulk data that simpler endpoints cannot provide (all followers, all replies to a tweet, community members, etc.). Always call `POST /api/v1/extractions/estimate` first. + +## Workflow Patterns + +| Workflow | Steps | +|----------|-------| +| **Set up real-time alerts** | `POST /monitors` -> `POST /webhooks` -> `POST /webhooks/{id}/test` | +| **Run a giveaway** | `GET /account` -> `POST /draws` | +| **Bulk extraction** | `POST /extractions/estimate` -> `POST /extractions` -> `GET /extractions/{id}` | +| **Compose optimized tweet** | `POST /compose` (step=compose -> refine -> score) | +| **Analyze tweet style** | `POST /styles` -> `GET /styles/{username}` -> `POST /compose` with `styleUsername` | +| **Post a tweet** | `GET /x/accounts` -> `POST /x/tweets` with `account` + `text` | +| **Get trending news** | `GET /radar` (free, all 7 sources, via `xquik` tool) -> `POST /compose` with trending topic | +| **Create automation** | `POST /automations` -> `POST /automations/{slug}/steps` -> `PATCH /automations/{slug}` (activate) | +| **Open support ticket** | `POST /support/tickets` -> `GET /support/tickets/{id}` | + +## Common Mistakes + +| Mistake | Fix | +|---------|-----| +| Combining free and paid calls in `Promise.all` | Call free endpoints first, then paid ones separately. A 402 in Promise.all kills all results | +| Using `compose` when user wants to send a tweet | `POST /compose` is for drafting. Use `POST /x/tweets` to send | +| Using `POST /x/tweets` when user wants help writing | Use the 3-step compose flow instead | +| Falling back to web search when API call fails | Use free data already fetched (radar, styles, compose). Never discard it | +| Not checking subscription before paid calls | Always attempt the call. Handle 402 by calling `POST /subscribe` for checkout URL | +| Passing API keys in code | Auth is injected automatically. Never include keys | +| Using `explore` for API calls | `explore` is read-only spec search. Use `xquik` for actual API calls | +| Looking up follow/DM by username | Follow and DM endpoints need numeric user ID. Look up via `GET /x/users/{username}` first | + +## Unsupported Operations + +These are NOT available via the MCP server: + +- API key management (create, list, delete) +- File export (CSV, XLSX, Markdown) +- Account locale update +- Scheduled tweets + +- Direct X search (use extraction `tweet_search_extractor` for bulk search) + +## Cost Reference + +- **Free**: account info, compose (all steps), styles (cached lookup/save/delete/compare), drafts, radar (via `xquik` tool, all 7 sources), subscribe, API keys, bot endpoints, integrations, X account management, automations (create, list, update, delete, steps), support tickets, credits (balance check, top up) +- **Subscription required**: tweet search, user lookup, tweet lookup, follow check, media download (first only, cached free), extractions, draws, style analysis (X API refresh), performance analysis, trends, all write actions (tweet, like, retweet, follow, DM, profile, media upload, communities) diff --git a/skills/.curated/x-twitter-scraper/references/python-examples.md b/skills/.curated/x-twitter-scraper/references/python-examples.md new file mode 100644 index 00000000..79055d21 --- /dev/null +++ b/skills/.curated/x-twitter-scraper/references/python-examples.md @@ -0,0 +1,151 @@ +# Xquik Python Examples + +Python equivalents of the JavaScript examples in SKILL.md. + +## Authentication + +```python +import requests + +API_KEY = "xq_YOUR_KEY_HERE" +BASE = "https://xquik.com/api/v1" +HEADERS = {"x-api-key": API_KEY, "Content-Type": "application/json"} +``` + +## Retry with Exponential Backoff + +```python +import time, random + +def xquik_fetch(path, method="GET", json_body=None, max_retries=3): + base_delay = 1.0 + + for attempt in range(max_retries + 1): + response = requests.request( + method, + f"{BASE}{path}", + headers=HEADERS, + json=json_body, + ) + + if response.ok: + return response.json() + + retryable = response.status_code == 429 or response.status_code >= 500 + if not retryable or attempt == max_retries: + error = response.json() + raise Exception(f"Xquik API {response.status_code}: {error['error']}") + + retry_after = response.headers.get("Retry-After") + delay = int(retry_after) if retry_after else base_delay * (2 ** attempt) + random.uniform(0, 1) + time.sleep(delay) +``` + +## Extraction Workflow + +```python +# Step 1: Estimate +estimate = xquik_fetch("/extractions/estimate", method="POST", json_body={ + "toolType": "reply_extractor", + "targetTweetId": "1893704267862470862", +}) + +if not estimate["allowed"]: + print(f"Would exceed quota: {estimate['projectedPercent']}%") + exit() + +# Step 2: Create job +job = xquik_fetch("/extractions", method="POST", json_body={ + "toolType": "reply_extractor", + "targetTweetId": "1893704267862470862", +}) + +# Step 3: Poll until complete (large jobs may return "running") +while job["status"] in ("pending", "running"): + time.sleep(2) + job = xquik_fetch(f"/extractions/{job['id']}") + +# Step 4: Get results +cursor = None +results = [] + +while True: + path = f"/extractions/{job['id']}" + if cursor: + path += f"?after={cursor}" + page = xquik_fetch(path) + results.extend(page["results"]) + + if not page["hasMore"]: + break + cursor = page["nextCursor"] + +print(f"Extracted {len(results)} results") +``` + +## Giveaway Draw + +```python +# Create draw with all filters +draw = xquik_fetch("/draws", method="POST", json_body={ + "tweetUrl": "https://x.com/burakbayir/status/1893456789012345678", + "winnerCount": 3, + "backupCount": 2, + "uniqueAuthorsOnly": True, + "mustRetweet": True, + "mustFollowUsername": "burakbayir", + "filterMinFollowers": 50, + "filterAccountAgeDays": 30, + "requiredKeywords": ["giveaway"], +}) + +# Get winners +details = xquik_fetch(f"/draws/{draw['id']}") +for winner in details["winners"]: + role = "BACKUP" if winner["isBackup"] else "WINNER" + print(f"{role} #{winner['position']}: @{winner['authorUsername']}") +``` + +## Webhook Handler (Flask) + +```python +import hmac, hashlib, json, os +from flask import Flask, request + +app = Flask(__name__) +WEBHOOK_SECRET = os.environ["XQUIK_WEBHOOK_SECRET"] +processed_hashes = set() # Use Redis/DB in production + +def verify_signature(payload: bytes, signature: str, secret: str) -> bool: + expected = "sha256=" + hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() + return hmac.compare_digest(expected, signature) + +EVENT_HANDLERS = { + "tweet.new": lambda u, d: print(f"New tweet from @{u}: {d['text']}"), + "tweet.reply": lambda u, d: print(f"Reply from @{u}: {d['text']}"), + "tweet.quote": lambda u, d: print(f"Quote from @{u}: {d['text']}"), + "tweet.retweet": lambda u, d: print(f"Retweet by @{u}"), + "follower.gained": lambda u, d: print(f"@{u} gained follower: @{d['followerUsername']}"), + "follower.lost": lambda u, d: print(f"@{u} lost follower: @{d['followerUsername']}"), +} + +@app.route("/webhook", methods=["POST"]) +def webhook(): + signature = request.headers.get("X-Xquik-Signature", "") + payload = request.get_data() + + if not verify_signature(payload, signature, WEBHOOK_SECRET): + return "Invalid signature", 401 + + payload_hash = hashlib.sha256(payload).hexdigest() + if payload_hash in processed_hashes: + return "Already processed", 200 + processed_hashes.add(payload_hash) + + event = json.loads(payload) + handler = EVENT_HANDLERS.get(event["eventType"]) + if handler: + handler(event["username"], event["data"]) + + return "OK", 200 +``` diff --git a/skills/.curated/x-twitter-scraper/references/types.md b/skills/.curated/x-twitter-scraper/references/types.md new file mode 100644 index 00000000..800b5f30 --- /dev/null +++ b/skills/.curated/x-twitter-scraper/references/types.md @@ -0,0 +1,1163 @@ +# Xquik TypeScript Type Definitions + +Copy-pasteable TypeScript types for all Xquik API objects. + +## Contents + +- [Account](#account) +- [API Keys](#api-keys) +- [Credits](#credits) +- [Monitors](#monitors) +- [Events](#events) +- [Webhooks](#webhooks) +- [Draws](#draws) +- [Extractions](#extractions) +- [X API](#x-api) +- [Trends](#trends) +- [Automations](#automations) +- [Support](#support) +- [Error](#error) +- [Request Bodies](#request-bodies) +- [MCP Output Schemas](#mcp-output-schemas) + +```typescript +// ─── Account ───────────────────────────────────────────── + +interface Account { + plan: "active" | "inactive"; + pricingVersion: number; + monitorsAllowed: number; + monitorsUsed: number; + currentPeriod?: { + start: string; + end: string; + usagePercent: number; + }; +} + +// ─── Credits ──────────────────────────────────────────── + +interface CreditBalance { + balance: number; // Current credit balance + lifetimePurchased: number; // Total credits purchased + lifetimeUsed: number; // Total credits consumed + autoTopUp: boolean; // Whether auto top-up is enabled +} + +interface CreditTopUpResponse { + url: string; // Stripe checkout URL +} + +// ─── API Keys ──────────────────────────────────────────── + +interface ApiKeyCreated { + id: string; + fullKey: string; + prefix: string; + name: string; + createdAt: string; +} + +interface ApiKey { + id: string; + name: string; + prefix: string; + isActive: boolean; + createdAt: string; + lastUsedAt?: string; +} + +// ─── Monitors ──────────────────────────────────────────── + +interface Monitor { + id: string; + username: string; + xUserId: string; + eventTypes: EventType[]; + isActive: boolean; + createdAt: string; +} + +type EventType = + | "tweet.new" + | "tweet.quote" + | "tweet.reply" + | "tweet.retweet" + | "follower.gained" + | "follower.lost"; + +// ─── Events ────────────────────────────────────────────── + +interface Event { + id: string; + type: EventType; + monitorId: string; + username: string; + occurredAt: string; + data: EventData; + xEventId?: string; +} + +// Tweet events (tweet.new, tweet.reply, tweet.quote, tweet.retweet) +interface TweetEventData { + tweetId: string; + text: string; + metrics: { + likes: number; + retweets: number; + replies: number; + }; + // tweet.quote only + quotedTweetId?: string; + quotedUsername?: string; + // tweet.reply only + inReplyToTweetId?: string; + inReplyToUsername?: string; + // tweet.retweet only + retweetedTweetId?: string; + retweetedUsername?: string; +} + +// Follower events (follower.gained, follower.lost) +interface FollowerEventData { + followerId: string; + followerUsername: string; + followerName: string; + followerFollowersCount: number; + followerVerified: boolean; +} + +type EventData = TweetEventData | FollowerEventData; + +interface EventList { + events: Event[]; + hasMore: boolean; + nextCursor?: string; +} + +// ─── Webhooks ──────────────────────────────────────────── + +interface WebhookCreated { + id: string; + url: string; + eventTypes: EventType[]; + secret: string; + createdAt: string; +} + +interface Webhook { + id: string; + url: string; + eventTypes: EventType[]; + isActive: boolean; + createdAt: string; +} + +interface Delivery { + id: string; + streamEventId: string; + status: "pending" | "delivered" | "failed" | "exhausted"; + attempts: number; + lastStatusCode?: number; + lastError?: string; + createdAt: string; + deliveredAt?: string; +} + +interface WebhookPayload { + eventType: EventType; + username: string; + data: EventData; +} + +// ─── Draws ─────────────────────────────────────────────── + +interface Draw { + id: string; + tweetId: string; + tweetUrl: string; + tweetText: string; + tweetAuthorUsername: string; + tweetLikeCount: number; + tweetRetweetCount: number; + tweetReplyCount: number; + tweetQuoteCount: number; + status: "pending" | "running" | "completed" | "failed"; + totalEntries: number; + validEntries: number; + createdAt: string; + drawnAt?: string; +} + +interface DrawListItem { + id: string; + tweetUrl: string; + status: "pending" | "running" | "completed" | "failed"; + totalEntries: number; + validEntries: number; + createdAt: string; + drawnAt?: string; +} + +interface DrawWinner { + position: number; + authorUsername: string; + tweetId: string; + isBackup: boolean; +} + +interface DrawList { + draws: DrawListItem[]; + hasMore: boolean; + nextCursor?: string; +} + +interface CreateDrawRequest { + tweetUrl: string; + winnerCount?: number; + backupCount?: number; + uniqueAuthorsOnly?: boolean; + mustRetweet?: boolean; + mustFollowUsername?: string; + filterMinFollowers?: number; + filterAccountAgeDays?: number; + filterLanguage?: string; + requiredKeywords?: string[]; + requiredHashtags?: string[]; + requiredMentions?: string[]; +} + +// ─── Extractions ───────────────────────────────────────── + +type ExtractionToolType = + | "article_extractor" + | "community_extractor" + | "community_moderator_explorer" + | "community_post_extractor" + | "community_search" + | "follower_explorer" + | "following_explorer" + | "list_follower_explorer" + | "list_member_extractor" + | "list_post_extractor" + | "mention_extractor" + | "people_search" + | "post_extractor" + | "quote_extractor" + | "reply_extractor" + | "repost_extractor" + | "space_explorer" + | "thread_extractor" + | "tweet_search_extractor" + | "verified_follower_explorer"; + +interface ExtractionJob { + id: string; + toolType: ExtractionToolType; + status: "pending" | "running" | "completed" | "failed"; + totalResults: number; + targetTweetId?: string; + targetUsername?: string; + targetUserId?: string; + targetCommunityId?: string; + targetListId?: string; + targetSpaceId?: string; + searchQuery?: string; + resultsLimit?: number; // Max results to extract. Stops early instead of fetching all. Omit for all. + errorMessage?: string; + createdAt: string; + completedAt?: string; +} + +interface ExtractionResult { + id: string; + xUserId: string; + xUsername?: string; + xDisplayName?: string; + xFollowersCount?: number; + xVerified?: boolean; + xProfileImageUrl?: string; + tweetId?: string; + tweetText?: string; + tweetCreatedAt?: string; + createdAt: string; +} + +interface ExtractionList { + extractions: ExtractionJob[]; + hasMore: boolean; + nextCursor?: string; +} + +interface ExtractionEstimate { + allowed: boolean; + source: "replyCount" | "retweetCount" | "quoteCount" | "followers" | "unknown"; + estimatedResults: number; + usagePercent: number; + projectedPercent: number; + error?: string; +} + +interface CreateExtractionRequest { + toolType: ExtractionToolType; + targetTweetId?: string; + targetUsername?: string; + targetCommunityId?: string; + targetListId?: string; + targetSpaceId?: string; + searchQuery?: string; + resultsLimit?: number; // Max results to extract. Stops early instead of fetching all. Omit for all. + // Tweet search filters (tweet_search_extractor only) + fromUser?: string; + toUser?: string; + mentioning?: string; + language?: string; + sinceDate?: string; // YYYY-MM-DD + untilDate?: string; // YYYY-MM-DD + mediaType?: 'images' | 'videos' | 'gifs' | 'media'; + minFaves?: number; + minRetweets?: number; + minReplies?: number; + verifiedOnly?: boolean; + replies?: 'include' | 'exclude' | 'only'; + retweets?: 'include' | 'exclude' | 'only'; + exactPhrase?: string; + excludeWords?: string; + advancedQuery?: string; +} + +// ─── X API ─────────────────────────────────────────────── + +interface TweetMediaItem { + mediaUrl: string; + type: string; // "photo" | "video" | "animated_gif" + url: string; +} + +interface Tweet { + id: string; + text: string; + createdAt?: string; + retweetCount: number; + replyCount: number; + likeCount: number; + quoteCount: number; + viewCount: number; + bookmarkCount: number; + media?: TweetMediaItem[]; +} + +interface TweetAuthor { + id: string; + username: string; + followers: number; + verified: boolean; + profilePicture?: string; +} + +interface TweetSearchResult { + id: string; + text: string; + createdAt: string; + likeCount: number; // Omitted if unavailable + retweetCount: number; // Omitted if unavailable + replyCount: number; // Omitted if unavailable + media?: TweetMediaItem[]; + author: { + id: string; + username: string; + name: string; + verified: boolean; + }; +} + +interface UserProfile { + id: string; + username: string; + name: string; + description?: string; + followers?: number; + following?: number; + verified?: boolean; + profilePicture?: string; + location?: string; + createdAt?: string; + statusesCount?: number; +} + +interface FollowerCheck { + sourceUsername: string; + targetUsername: string; + isFollowing: boolean; + isFollowedBy: boolean; +} + +// ─── User Activity ────────────────────────────────────── + +interface UserTweetsResponse { + tweets: Tweet[]; + hasMore: boolean; + nextCursor?: string; +} + +interface UserLikesResponse { + tweets: Tweet[]; + hasMore: boolean; + nextCursor?: string; +} + +interface UserMediaResponse { + tweets: Tweet[]; + hasMore: boolean; + nextCursor?: string; +} + +interface TweetFavoritersResponse { + users: UserProfile[]; + hasMore: boolean; + nextCursor?: string; +} + +interface FollowersYouKnowResponse { + users: UserProfile[]; + hasMore: boolean; + nextCursor?: string; +} + +// ─── Bookmarks & Timeline ─────────────────────────────── + +interface BookmarksResponse { + tweets: Tweet[]; + hasMore: boolean; + nextCursor?: string; +} + +interface BookmarkFolder { + id: string; + name: string; +} + +interface BookmarkFoldersResponse { + folders: BookmarkFolder[]; +} + +interface NotificationsResponse { + notifications: Notification[]; + hasMore: boolean; + nextCursor?: string; +} + +interface TimelineResponse { + tweets: Tweet[]; + hasMore: boolean; + nextCursor?: string; +} + +interface DmHistoryResponse { + messages: DmMessage[]; + hasMore: boolean; + nextCursor?: string; +} + +interface DmMessage { + id: string; + text: string; + senderId: string; + createdAt: string; + media?: TweetMediaItem[]; +} + +// ─── X Articles ───────────────────────────────────────── + +interface Article { + title: string; + coverImage?: string; + bodyHtml: string; + likeCount: number; + retweetCount: number; + replyCount: number; + viewCount: number; + bookmarkCount: number; + author: { + id: string; + username: string; + name: string; + }; +} + +// ─── Radar ─────────────────────────────────────────────── + +type RadarSource = + | "github" + | "google_trends" + | "hacker_news" + | "polymarket" + | "reddit" + | "trustmrr" + | "wikipedia"; + +type RadarCategory = + | "general" + | "tech" + | "dev" + | "science" + | "culture" + | "politics" + | "business" + | "entertainment"; + +interface RadarItem { + id: string; + title: string; + description?: string; + url?: string; + imageUrl?: string; + source: RadarSource; + sourceId: string; + category: RadarCategory; + region: string; + language: string; + score: number; + metadata: Record; + publishedAt: string; + createdAt: string; +} + +// ─── Download Media ───────────────────────────────────── + +interface DownloadMediaRequest { + tweetInput?: string; // Tweet URL or numeric tweet ID (single mode) + tweetIds?: string[]; // Array of tweet URLs or IDs (bulk mode, max 50). Exactly 1 of tweetInput or tweetIds required. +} + +interface DownloadMediaSingleResponse { + tweetId: string; // Resolved tweet ID + galleryUrl: string; // Shareable gallery page URL + cacheHit: boolean; // true if served from cache (no usage consumed) +} + +interface DownloadMediaBulkResponse { + galleryUrl: string; // Combined gallery page URL + totalTweets: number; // Number of tweets processed + totalMedia: number; // Total media items downloaded +} + +// ─── Trends ────────────────────────────────────────────── + +interface Trend { + name: string; + description?: string; + rank?: number; + query?: string; +} + +interface TrendList { + trends: Trend[]; + total: number; + woeid: number; +} + +// ─── Automations ──────────────────────────────────────── + +type TriggerType = "monitor_event" | "schedule" | "search" | "webhook_inbound"; + +type StepType = "action" | "condition" | "extraction"; + +type ActionType = + | "create_tweet" + | "follow" + | "like" + | "reply_tweet" + | "retweet" + | "send_dm" + | "send_email" + | "send_telegram" + | "unfollow"; + +interface AutomationFlow { + id: string; + name: string; + slug: string; + triggerType: TriggerType; + triggerConfig: Record; + isActive: boolean; + runCount: string; + lastRunAt: string | null; + minIntervalSeconds: number; + pausedReason: string | null; + templateSlug: string | null; + xAccountId: string | null; + createdAt: string; + updatedAt: string; +} + +interface AutomationStep { + id: string; + flowId: string; + stepType: StepType; + actionType: ActionType | null; + extractionType: string | null; + branch: "main" | "if_true" | "if_false"; + config: Record; + position: number; + positionX: number | null; + positionY: number | null; + parentStepId: string | null; + outputName: string | null; + createdAt: string; +} + +interface AutomationRun { + id: string; + status: string; + startedAt: string; + completedAt: string | null; + errorMessage: string | null; + triggeredBy: string; +} + +interface AutomationDetail extends AutomationFlow { + steps: AutomationStep[]; + recentRuns: AutomationRun[]; +} + +interface CreateAutomationRequest { + name: string; + triggerType: TriggerType; + triggerConfig: Record; + templateSlug?: string; +} + +interface UpdateAutomationRequest { + expectedUpdatedAt: string; + name?: string; + triggerType?: TriggerType; + triggerConfig?: Record; + isActive?: boolean; + triggerPositionX?: number | null; + triggerPositionY?: number | null; +} + +interface AddStepRequest { + stepType: StepType; + branch: "main" | "if_true" | "if_false"; + config: Record; + position?: number; + parentStepId?: string; + actionType?: ActionType; + extractionType?: string; + outputName?: string; +} + +// ─── Support ──────────────────────────────────────────── + +interface SupportTicket { + id: string; + subject: string; + status: string; + createdAt: string; + updatedAt: string; +} + +interface SupportMessage { + id: string; + body: string; + sender: string; + createdAt: string; +} + +interface CreateTicketRequest { + subject: string; + body: string; +} + +// ─── Error ─────────────────────────────────────────────── + +interface ApiError { + error: string; + limit?: number; +} + +// ─── Request Bodies ────────────────────────────────────── + +interface CreateMonitorRequest { + username: string; + eventTypes: EventType[]; +} + +interface UpdateMonitorRequest { + eventTypes?: EventType[]; + isActive?: boolean; +} + +interface CreateWebhookRequest { + url: string; + eventTypes: EventType[]; +} + +interface UpdateWebhookRequest { + url?: string; + eventTypes?: EventType[]; + isActive?: boolean; +} + +interface CreateApiKeyRequest { + name?: string; +} + +// --- Tweet Style Cache --- + +interface TweetStyleCache { + xUsername: string; + tweetCount: number; + isOwnAccount: boolean; + fetchedAt: string; // ISO 8601 + tweets: CachedTweet[]; +} + +interface CachedTweet { + id: string; + text: string; + authorUsername: string; + createdAt: string; // ISO 8601 + media?: TweetMediaItem[]; +} + +interface TweetStyleSummary { + xUsername: string; + tweetCount: number; + isOwnAccount: boolean; + fetchedAt: string; +} + +interface StyleComparison { + style1: TweetStyleCache; + style2: TweetStyleCache; +} + +interface StylePerformance { + xUsername: string; + tweetCount: number; + tweets: PerformanceTweet[]; +} + +interface PerformanceTweet { + id: string; + text: string; + likeCount: number; + retweetCount: number; + replyCount: number; + quoteCount: number; + viewCount: number; + bookmarkCount: number; +} + +// --- Tweet Drafts --- + +interface TweetDraft { + id: string; + text: string; + topic?: string; + goal?: "engagement" | "followers" | "authority" | "conversation"; + createdAt: string; // ISO 8601 + updatedAt: string; // ISO 8601 +} + +interface TweetDraftList { + drafts: TweetDraft[]; + afterCursor: string | null; + hasMore: boolean; +} + +// --- Account Identity --- + +interface XIdentityResponse { + success: boolean; + xUsername: string; +} +``` + +## REST API vs MCP Field Naming + +The REST API and MCP server use different field names for the same data. Map these when switching between interfaces: + +| Type | REST API Field | MCP Field | +|------|---------------|-----------| +| **Monitor** | `username` | `xUsername` | +| **Event** | `type` | `eventType` | +| **Event** | `data` | `eventData` | +| **Event** | `monitorId` | `monitoredAccountId` | +| **UserProfile** | `followers` | `followersCount` | +| **UserProfile** | `following` | `followingCount` | +| **FollowerCheck** | `isFollowing` / `isFollowedBy` | `following` / `followedBy` | + +**MCP `get-user-info` returns a subset** of the full `UserProfile` type. Fields not returned by MCP: `verified`, `location`, `createdAt`, `statusesCount`. Use the REST API `GET /x/users/{username}` for the complete profile. + +## MCP Output Schemas + +MCP tools return structured data with these shapes. Field names differ from the REST API (see mapping table above). + +```typescript +// ─── MCP: get-user-info ───────────────────────────────── + +interface McpUserInfo { + username: string; // X username (without @) + name: string; // Display name + description: string; // User bio text + followersCount: number; // Number of followers + followingCount: number; // Number of accounts followed + profilePicture: string; // Profile picture URL + // Not returned: verified, location, createdAt, statusesCount + // Use REST GET /x/users/{username} for the full profile +} + +// ─── MCP: search-tweets ───────────────────────────────── + +interface McpSearchResult { + tweets: { + id: string; // Tweet ID (use with lookup-tweet for full metrics) + text: string; // Full tweet text + authorUsername: string; // X username of the tweet author + authorName: string; // Display name of the tweet author + createdAt: string; // ISO 8601 timestamp when tweet was posted + media?: { mediaUrl: string; type: string; url: string }[]; // Attached photos/videos + // No engagement metrics. Use lookup-tweet for those + }[]; +} + +// ─── MCP: lookup-tweet ────────────────────────────────── + +interface McpTweetLookup { + tweet: { + id: string; // Tweet ID + text: string; // Tweet text + likeCount: number; // Number of likes + retweetCount: number; // Number of retweets + replyCount: number; // Number of replies + quoteCount: number; // Number of quote tweets + viewCount: number; // Number of views + bookmarkCount: number; // Number of bookmarks + media?: { mediaUrl: string; type: string; url: string }[]; // Attached photos/videos + }; + author?: { // Tweet author details + id: string; // Author user ID + username: string; // Author X username + followers: number; // Author follower count + verified: boolean; // Whether the author is verified + }; +} + +// ─── MCP: check-follow ───────────────────────────────── + +interface McpFollowCheck { + following: boolean; // Whether the source follows the target + followedBy: boolean; // Whether the target follows the source +} + +// ─── MCP: get-events ──────────────────────────────────── + +interface McpEventList { + events: { + id: string; // Event ID (use with get-event for full details) + xUsername: string; // Username of the monitored account + eventType: string; // Event type (tweet.new, tweet.reply, etc.) + eventData: unknown; // Full event payload (tweet text, author, metrics) + monitoredAccountId: string; // ID of the monitored account + createdAt: string; // ISO 8601 when event was recorded + occurredAt: string; // ISO 8601 when event occurred on X + }[]; + hasMore: boolean; // Whether more results are available + nextCursor?: string; // Pass as afterCursor to fetch the next page +} + +// ─── MCP: list-monitors ───────────────────────────────── + +interface McpMonitorList { + monitors: { + id: string; // Monitor ID (use with remove-monitor, get-events monitorId filter) + xUsername: string; // Monitored X username + eventTypes: string[]; // Subscribed event types + isActive: boolean; // Whether the monitor is currently active + createdAt: string; // ISO 8601 timestamp + }[]; +} + +// ─── MCP: add-webhook ─────────────────────────────────── + +interface McpWebhookCreated { + id: string; // Webhook ID + url: string; // HTTPS endpoint URL + eventTypes: string[]; // Event types delivered to this webhook + isActive: boolean; // Whether the webhook is active + createdAt: string; // ISO 8601 timestamp + secret: string; // HMAC signing secret for verifying webhook payloads. Store securely. +} + +// ─── MCP: test-webhook ────────────────────────────────── + +interface McpWebhookTest { + success: boolean; + statusCode: number; + error?: string; +} + +// ─── MCP: run-extraction ──────────────────────────────── + +interface McpExtractionJob { + id: string; // Extraction job ID (use with get-extraction for results) + toolType: string; // Extraction tool type used + status: string; // Job status + totalResults: number; // Number of results extracted +} + +// ─── MCP: estimate-extraction ─────────────────────────── + +interface McpExtractionEstimate { + allowed?: boolean; // Whether the extraction is allowed within budget + estimatedResults?: number; // Estimated number of results + projectedPercent?: number; // Projected usage percent after extraction + usagePercent?: number; // Current usage percent of monthly quota + source?: string; // Data source used for estimation + error?: string; // Error message if estimation failed +} + +// ─── MCP: run-draw ────────────────────────────────────── + +interface McpDrawResult { + id: string; // Draw ID (use with get-draw for full details) + tweetId: string; // Giveaway tweet ID + totalEntries: number; // Total reply count before filtering + validEntries: number; // Valid entries after filtering + winners: { + position: number; // Winner position (1-based) + authorUsername: string; // X username of the winner + tweetId: string; // Tweet ID of the winning reply + isBackup: boolean; // Whether this is a backup winner + }[]; +} + +// ─── MCP: get-draw ────────────────────────────────────── + +interface McpDrawDetails { + draw: { + id: string; // Draw ID + status: string; // Draw status (completed, failed) + createdAt: string; // ISO 8601 timestamp + drawnAt?: string; // ISO 8601 timestamp when winners were drawn + totalEntries: number; // Total reply count before filtering + validEntries: number; // Entries remaining after filters applied + tweetId: string; // Giveaway tweet ID + tweetUrl: string; // Full URL of the giveaway tweet + tweetText: string; // Giveaway tweet text + tweetAuthorUsername: string; // Username of the giveaway tweet author + tweetLikeCount: number; // Tweet like count at draw time + tweetRetweetCount: number; // Tweet retweet count at draw time + tweetReplyCount: number; // Tweet reply count at draw time + tweetQuoteCount: number; // Tweet quote count at draw time + }; + winners: { + position: number; // Winner position (1-based) + authorUsername: string; // X username of the winner + tweetId: string; // Tweet ID of the winning reply + isBackup: boolean; // Whether this is a backup winner + }[]; +} + +// ─── MCP: get-account ─────────────────────────────────── + +interface McpAccount { + plan: string; // Current plan name (free or subscriber) + monitorsAllowed: number; // Maximum monitors allowed on current plan + monitorsUsed: number; // Number of active monitors + currentPeriod?: { // Current billing period (present only with active subscription) + start: string; // ISO 8601 period start date + end: string; // ISO 8601 period end date + usagePercent: number; // Percent of monthly quota consumed + }; +} + +// ─── MCP: get-trends ──────────────────────────────────── + +interface McpTrends { + woeid: number; + total: number; + trends: { + name: string; // Trend name or hashtag + rank?: number; // Trend rank position + description?: string; // Trend description or context + query?: string; // Search query to find tweets for this trend + }[]; +} + +// ─── MCP: subscribe ──────────────────────────────────── + +interface McpSubscribe { + status: "already_subscribed" | "checkout_created" | "payment_issue"; + url: string; // Stripe Checkout or Customer Portal URL. Open in browser. + message: string; // Human-readable status message +} + +// ─── MCP: compose-tweet ──────────────────────────────── + +interface McpComposeTweet { + algorithmInsights: { + name: string; // Signal name from PhoenixScores + polarity: "positive" | "negative"; // Whether this signal helps or hurts ranking + description: string; // What this signal measures + }[]; + contentRules: { + rule: string; // Actionable content rule + description: string; // Why this rule matters based on algorithm architecture + }[]; + engagementMultipliers: { + action: string; // Engagement action (e.g. reply chain, quote tweet) + multiplier: string; // Relative value compared to a like (e.g. "27x a like") + source: string; // Data source for this multiplier + }[]; + engagementVelocity: string; // How early engagement velocity affects distribution + followUpQuestions: string[]; // Questions for the AI to ask the user before composing + scorerWeights: { + signal: string; // Signal name in the scoring model + weight: number; // Weight applied to predicted probability + context: string; // Practical meaning of this weight + }[]; + topPenalties: string[]; // Most severe negative signals to avoid + source: string; // Attribution to algorithm source code +} + +// ─── MCP: refine-tweet ───────────────────────────────── + +interface McpRefineTweet { + compositionGuidance: string[]; // Targeted guidance based on user preferences + examplePatterns: { + pattern: string; // Tweet structure template + description: string; // What this pattern achieves + }[]; +} + +// ─── MCP: score-tweet ────────────────────────────────── + +interface McpScoreTweet { + totalChecks: number; // Total number of checks performed + passedCount: number; // Number of checks that passed + topSuggestion: string; // Highest-impact improvement suggestion + checklist: { + factor: string; // What was checked + passed: boolean; // Whether the check passed + suggestion?: string; // Improvement suggestion (present only if failed) + }[]; +} + +// ─── X Accounts (Connected) ────────────────────────── + +interface ConnectedXAccount { + id: string; // Unique account ID + username: string; // X username + displayName?: string; // Display name on X + isActive: boolean; // Whether the connection is active + createdAt: string; // ISO 8601 timestamp +} + +interface ConnectXAccountRequest { + username: string; // X username (@ auto-stripped) + email: string; // Email associated with X account + password: string; // Password (encrypted at rest) + totp_secret?: string; // TOTP base32 secret for 2FA accounts + proxy_country?: string; // Preferred proxy region (e.g. "US") +} + +interface ReauthXAccountRequest { + password: string; // Current password + totp_secret?: string; // TOTP secret if 2FA enabled +} + +// ─── X Write ────────────────────────────────────────── + +interface CreateTweetRequest { + account: string; // Connected X username or account ID + text: string; // Tweet text (280 chars, or 25,000 if is_note_tweet) + reply_to_tweet_id?: string; // Tweet ID to reply to + attachment_url?: string; // URL to attach as card + community_id?: string; // Community ID to post into + is_note_tweet?: boolean; // Long-form note tweet (up to 25,000 chars) + media_ids?: string[]; // Media IDs from POST /x/media (max 4 images or 1 video) +} + +interface CreateTweetResponse { + tweetId: string; // ID of the newly created tweet + success: boolean; // Always true on success +} + +interface WriteActionRequest { + account: string; // Connected X username or account ID +} + +interface SendDmRequest { + account: string; // Connected X username or account ID + text: string; // Message text + media_ids?: string[]; // Media IDs to attach + reply_to_message_id?: string; // Message ID to reply to +} + +interface UpdateProfileRequest { + account: string; // Connected X username or account ID + name?: string; // Display name + description?: string; // Bio + location?: string; // Location + url?: string; // Website URL +} + +// ─── Integrations ───────────────────────────────────── + +// Integration event types differ from monitor event types: +// includes system events (draw/extraction) but NOT follower events +type IntegrationEventType = + | "tweet.new" + | "tweet.quote" + | "tweet.reply" + | "tweet.retweet" + | "draw.completed" + | "extraction.completed" + | "extraction.failed"; + +interface Integration { + id: string; // Unique integration ID + type: string; // Integration type ("telegram") + name: string; // Human-readable name + config: Record; // Type-specific config (Telegram: { chatId }) + eventTypes: IntegrationEventType[]; // Subscribed event types + isActive: boolean; // Whether the integration is active + createdAt: string; // ISO 8601 timestamp + updatedAt: string; // ISO 8601 timestamp +} + +interface CreateIntegrationRequest { + type: string; // "telegram" + name: string; // Human-readable name + config: { chatId: string }; // Telegram config + eventTypes: IntegrationEventType[]; // Event types to subscribe to +} + +interface UpdateIntegrationRequest { + name?: string; // New name + eventTypes?: string[]; // New event types + isActive?: boolean; // Activate/deactivate + silentPush?: boolean; // Silent notifications + scopeAllMonitors?: boolean; // Scope to all monitors + filters?: Record; // Filters + messageTemplate?: Record; // Custom message template +} + +interface IntegrationDelivery { + id: string; // Delivery ID + integrationId: string; // Integration ID + sourceType: string; // "monitor_event" | "extraction" | "draw" + sourceId: string; // Source record ID + eventType: string; // Event type + status: string; // "pending" | "delivered" | "failed" | "exhausted" + lastError?: string; // Last error message + attempts: number; // Delivery attempt count + deliveredAt?: string; // ISO 8601 timestamp + createdAt: string; // ISO 8601 timestamp +} +``` diff --git a/skills/.curated/x-twitter-scraper/references/webhooks.md b/skills/.curated/x-twitter-scraper/references/webhooks.md new file mode 100644 index 00000000..81f3d282 --- /dev/null +++ b/skills/.curated/x-twitter-scraper/references/webhooks.md @@ -0,0 +1,195 @@ +# Xquik Webhooks + +Receive real-time event notifications at your HTTPS endpoints with HMAC-SHA256 signature verification. + +## Setup + +1. Create at least 1 active monitor (`POST /monitors`) +2. Register a webhook endpoint (`POST /webhooks`) +3. Save the `secret` from the response (shown only once) +4. Build a handler that verifies signatures before processing + +## Webhook Payload + +Every delivery is a `POST` request to your URL with a JSON body: + +```json +{ + "eventType": "tweet.new", + "username": "elonmusk", + "data": { + "tweetId": "1893556789012345678", + "text": "Hello world", + "metrics": { "likes": 3200, "retweets": 890, "replies": 245 } + } +} +``` + +## Signature Verification + +The `X-Xquik-Signature` header contains: `sha256=` + HMAC-SHA256(secret, raw JSON body). + +### Node.js (Express) + +```javascript +import express from "express"; +import { createHmac, timingSafeEqual } from "node:crypto"; + +const WEBHOOK_SECRET = process.env.XQUIK_WEBHOOK_SECRET; + +function verifySignature(payload, signature, secret) { + const expected = "sha256=" + createHmac("sha256", secret).update(payload).digest("hex"); + return timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); +} + +app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => { + const signature = req.headers["x-xquik-signature"]; + const payload = req.body.toString(); + + if (!verifySignature(payload, signature, WEBHOOK_SECRET)) { + return res.status(401).send("Invalid signature"); + } + + const event = JSON.parse(payload); + + switch (event.eventType) { + case "tweet.new": + console.log(`New tweet from @${event.username}: ${event.data.text}`); + break; + case "tweet.reply": + console.log(`Reply from @${event.username}: ${event.data.text}`); + break; + case "follower.gained": + console.log(`@${event.username} gained a follower`); + break; + } + + res.status(200).send("OK"); +}); +``` + +### Python (Flask) + +```python +import hmac +import hashlib +import os +from flask import Flask, request + +app = Flask(__name__) +WEBHOOK_SECRET = os.environ["XQUIK_WEBHOOK_SECRET"] + +def verify_signature(payload: bytes, signature: str, secret: str) -> bool: + expected = "sha256=" + hmac.new( + secret.encode(), payload, hashlib.sha256 + ).hexdigest() + return hmac.compare_digest(expected, signature) + +@app.route("/webhook", methods=["POST"]) +def webhook(): + signature = request.headers.get("X-Xquik-Signature", "") + payload = request.get_data() + + if not verify_signature(payload, signature, WEBHOOK_SECRET): + return "Invalid signature", 401 + + event = request.get_json() + + if event["eventType"] == "tweet.new": + print(f"New tweet from @{event['username']}: {event['data']['text']}") + + return "OK", 200 +``` + +### Go + +```go +package main + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "os" +) + +var webhookSecret = os.Getenv("XQUIK_WEBHOOK_SECRET") + +func verifySignature(payload []byte, signature, secret string) bool { + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write(payload) + expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) + return hmac.Equal([]byte(expected), []byte(signature)) +} + +func webhookHandler(w http.ResponseWriter, r *http.Request) { + payload, _ := io.ReadAll(r.Body) + signature := r.Header.Get("X-Xquik-Signature") + + if !verifySignature(payload, signature, webhookSecret) { + http.Error(w, "Invalid signature", http.StatusUnauthorized) + return + } + + var event struct { + EventType string `json:"eventType"` + Username string `json:"username"` + Data struct { + Text string `json:"text"` + } `json:"data"` + } + json.Unmarshal(payload, &event) + + fmt.Printf("[%s] @%s: %s\n", event.EventType, event.Username, event.Data.Text) + fmt.Fprint(w, "OK") +} +``` + +## Security Checklist + +- **Verify before processing.** Never process unverified payloads +- **Use constant-time comparison.** `timingSafeEqual` (Node.js), `hmac.compare_digest` (Python), `hmac.Equal` (Go) +- **Use the raw request body.** Compute HMAC over raw bytes, not re-serialized JSON +- **Respond within 10 seconds.** Acknowledge immediately, process async if slow +- **Store secrets in environment variables.** Never hardcode + +## Idempotency + +Webhook deliveries can retry on failure, delivering the same event multiple times. Deduplicate by hashing the raw payload: + +```javascript +import { createHash } from "node:crypto"; + +const processedPayloads = new Set(); // Use Redis/DB in production + +const payloadHash = createHash("sha256").update(rawPayload).digest("hex"); +if (processedPayloads.has(payloadHash)) { + return res.status(200).send("Already processed"); +} +processedPayloads.add(payloadHash); +``` + +## Retry Policy + +Failed deliveries are retried up to 5 times with exponential backoff. Delivery statuses: `pending`, `delivered`, `failed`, `exhausted`. + +Check delivery status: `GET /webhooks/{id}/deliveries`. + +## Local Testing + +Use [ngrok](https://ngrok.com) to expose a local server: + +```bash +# Terminal 1: Start your webhook server +node server.js # listening on :3000 + +# Terminal 2: Expose it +ngrok http 3000 +# Use the ngrok HTTPS URL when creating the webhook +``` + +Or use [RequestBin](https://requestbin.com) for quick inspection without running a server.