x-developer 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/package.json +33 -0
- package/skills/x-twitter-scraper/SKILL.md +421 -0
- package/skills/x-twitter-scraper/metadata.json +14 -0
- package/skills/x-twitter-scraper/references/api-endpoints.md +1607 -0
- package/skills/x-twitter-scraper/references/draws.md +57 -0
- package/skills/x-twitter-scraper/references/extractions.md +229 -0
- package/skills/x-twitter-scraper/references/mcp-setup.md +207 -0
- package/skills/x-twitter-scraper/references/mcp-tools.md +151 -0
- package/skills/x-twitter-scraper/references/pricing.md +132 -0
- package/skills/x-twitter-scraper/references/python-examples.md +152 -0
- package/skills/x-twitter-scraper/references/types.md +1163 -0
- package/skills/x-twitter-scraper/references/webhooks.md +198 -0
- package/skills/x-twitter-scraper/references/workflows.md +194 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Xquik Webhooks
|
|
2
|
+
|
|
3
|
+
Receive real-time event notifications at your HTTPS endpoints with HMAC-SHA256 signature verification.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Create at least 1 active monitor (`POST /monitors`)
|
|
8
|
+
2. Register a webhook endpoint (`POST /webhooks`)
|
|
9
|
+
3. Save the `secret` from the response (shown only once)
|
|
10
|
+
4. Build a handler that verifies signatures before processing
|
|
11
|
+
|
|
12
|
+
## Webhook Payload
|
|
13
|
+
|
|
14
|
+
Every delivery is a `POST` request to your URL with a JSON body:
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"eventType": "tweet.new",
|
|
19
|
+
"username": "elonmusk",
|
|
20
|
+
"data": {
|
|
21
|
+
"tweetId": "1893556789012345678",
|
|
22
|
+
"text": "Hello world",
|
|
23
|
+
"metrics": { "likes": 3200, "retweets": 890, "replies": 245 }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Signature Verification
|
|
29
|
+
|
|
30
|
+
The `X-Xquik-Signature` header contains: `sha256=` + HMAC-SHA256(secret, raw JSON body).
|
|
31
|
+
|
|
32
|
+
### Node.js (Express)
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
import express from "express";
|
|
36
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
37
|
+
|
|
38
|
+
// This is the per-webhook secret from the POST /webhooks response, not a Xquik account credential
|
|
39
|
+
const WEBHOOK_SECRET = process.env.XQUIK_WEBHOOK_SECRET;
|
|
40
|
+
|
|
41
|
+
function verifySignature(payload, signature, secret) {
|
|
42
|
+
const expected = "sha256=" + createHmac("sha256", secret).update(payload).digest("hex");
|
|
43
|
+
return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
|
|
47
|
+
const signature = req.headers["x-xquik-signature"];
|
|
48
|
+
const payload = req.body.toString();
|
|
49
|
+
|
|
50
|
+
if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
|
|
51
|
+
return res.status(401).send("Invalid signature");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const event = JSON.parse(payload);
|
|
55
|
+
|
|
56
|
+
switch (event.eventType) {
|
|
57
|
+
case "tweet.new":
|
|
58
|
+
console.log(`New tweet from @${event.username}: ${event.data.text}`);
|
|
59
|
+
break;
|
|
60
|
+
case "tweet.reply":
|
|
61
|
+
console.log(`Reply from @${event.username}: ${event.data.text}`);
|
|
62
|
+
break;
|
|
63
|
+
case "follower.gained":
|
|
64
|
+
console.log(`@${event.username} gained a follower`);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
res.status(200).send("OK");
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Python (Flask)
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import hmac
|
|
76
|
+
import hashlib
|
|
77
|
+
import os
|
|
78
|
+
from flask import Flask, request
|
|
79
|
+
|
|
80
|
+
app = Flask(__name__)
|
|
81
|
+
# Per-webhook secret from POST /webhooks response, not a Xquik account credential
|
|
82
|
+
WEBHOOK_SECRET = os.environ["XQUIK_WEBHOOK_SECRET"]
|
|
83
|
+
|
|
84
|
+
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
|
|
85
|
+
expected = "sha256=" + hmac.new(
|
|
86
|
+
secret.encode(), payload, hashlib.sha256
|
|
87
|
+
).hexdigest()
|
|
88
|
+
return hmac.compare_digest(expected, signature)
|
|
89
|
+
|
|
90
|
+
@app.route("/webhook", methods=["POST"])
|
|
91
|
+
def webhook():
|
|
92
|
+
signature = request.headers.get("X-Xquik-Signature", "")
|
|
93
|
+
payload = request.get_data()
|
|
94
|
+
|
|
95
|
+
if not verify_signature(payload, signature, WEBHOOK_SECRET):
|
|
96
|
+
return "Invalid signature", 401
|
|
97
|
+
|
|
98
|
+
event = request.get_json()
|
|
99
|
+
|
|
100
|
+
if event["eventType"] == "tweet.new":
|
|
101
|
+
print(f"New tweet from @{event['username']}: {event['data']['text']}")
|
|
102
|
+
|
|
103
|
+
return "OK", 200
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Go
|
|
107
|
+
|
|
108
|
+
```go
|
|
109
|
+
package main
|
|
110
|
+
|
|
111
|
+
import (
|
|
112
|
+
"crypto/hmac"
|
|
113
|
+
"crypto/sha256"
|
|
114
|
+
"encoding/hex"
|
|
115
|
+
"encoding/json"
|
|
116
|
+
"fmt"
|
|
117
|
+
"io"
|
|
118
|
+
"net/http"
|
|
119
|
+
"os"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
// Per-webhook secret from POST /webhooks response, not a Xquik account credential
|
|
123
|
+
var webhookSecret = os.Getenv("XQUIK_WEBHOOK_SECRET")
|
|
124
|
+
|
|
125
|
+
func verifySignature(payload []byte, signature, secret string) bool {
|
|
126
|
+
mac := hmac.New(sha256.New, []byte(secret))
|
|
127
|
+
mac.Write(payload)
|
|
128
|
+
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
|
129
|
+
return hmac.Equal([]byte(expected), []byte(signature))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
|
133
|
+
payload, _ := io.ReadAll(r.Body)
|
|
134
|
+
signature := r.Header.Get("X-Xquik-Signature")
|
|
135
|
+
|
|
136
|
+
if !verifySignature(payload, signature, webhookSecret) {
|
|
137
|
+
http.Error(w, "Invalid signature", http.StatusUnauthorized)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
var event struct {
|
|
142
|
+
EventType string `json:"eventType"`
|
|
143
|
+
Username string `json:"username"`
|
|
144
|
+
Data struct {
|
|
145
|
+
Text string `json:"text"`
|
|
146
|
+
} `json:"data"`
|
|
147
|
+
}
|
|
148
|
+
json.Unmarshal(payload, &event)
|
|
149
|
+
|
|
150
|
+
fmt.Printf("[%s] @%s: %s\n", event.EventType, event.Username, event.Data.Text)
|
|
151
|
+
fmt.Fprint(w, "OK")
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Security Checklist
|
|
156
|
+
|
|
157
|
+
- **Verify before processing.** Never process unverified payloads
|
|
158
|
+
- **Use constant-time comparison.** `timingSafeEqual` (Node.js), `hmac.compare_digest` (Python), `hmac.Equal` (Go)
|
|
159
|
+
- **Use the raw request body.** Compute HMAC over raw bytes, not re-serialized JSON
|
|
160
|
+
- **Respond within 10 seconds.** Acknowledge immediately, process async if slow
|
|
161
|
+
- **Store secrets in environment variables.** Never hardcode
|
|
162
|
+
|
|
163
|
+
## Idempotency
|
|
164
|
+
|
|
165
|
+
Webhook deliveries can retry on failure, delivering the same event multiple times. Deduplicate by hashing the raw payload:
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
import { createHash } from "node:crypto";
|
|
169
|
+
|
|
170
|
+
const processedPayloads = new Set(); // Use Redis/DB in production
|
|
171
|
+
|
|
172
|
+
const payloadHash = createHash("sha256").update(rawPayload).digest("hex");
|
|
173
|
+
if (processedPayloads.has(payloadHash)) {
|
|
174
|
+
return res.status(200).send("Already processed");
|
|
175
|
+
}
|
|
176
|
+
processedPayloads.add(payloadHash);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Retry Policy
|
|
180
|
+
|
|
181
|
+
Failed deliveries are retried up to 5 times with exponential backoff. Delivery statuses: `pending`, `delivered`, `failed`, `exhausted`.
|
|
182
|
+
|
|
183
|
+
Check delivery status: `GET /webhooks/{id}/deliveries`.
|
|
184
|
+
|
|
185
|
+
## Local Testing
|
|
186
|
+
|
|
187
|
+
Use [ngrok](https://ngrok.com) to expose a local server:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Terminal 1: Start your webhook server
|
|
191
|
+
node server.js # listening on :3000
|
|
192
|
+
|
|
193
|
+
# Terminal 2: Expose it
|
|
194
|
+
ngrok http 3000
|
|
195
|
+
# Use the ngrok HTTPS URL when creating the webhook
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Or use [RequestBin](https://requestbin.com) for quick inspection without running a server.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Xquik Workflow Examples
|
|
2
|
+
|
|
3
|
+
Code examples for common integration patterns.
|
|
4
|
+
|
|
5
|
+
## Authentication
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
const API_KEY = "xq_YOUR_KEY_HERE";
|
|
9
|
+
const BASE = "https://xquik.com/api/v1";
|
|
10
|
+
const headers = { "x-api-key": API_KEY, "Content-Type": "application/json" };
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Retry with Exponential Backoff
|
|
14
|
+
|
|
15
|
+
Retry only `429` and `5xx`. Never retry `4xx` (except 429). Max 3 retries:
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
async function xquikFetch(path, options = {}) {
|
|
19
|
+
const baseDelay = 1000;
|
|
20
|
+
|
|
21
|
+
for (let attempt = 0; attempt <= 3; attempt++) {
|
|
22
|
+
const response = await fetch(`${BASE}${path}`, {
|
|
23
|
+
...options,
|
|
24
|
+
headers: { ...headers, ...options.headers },
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (response.ok) return response.json();
|
|
28
|
+
|
|
29
|
+
const retryable = response.status === 429 || response.status >= 500;
|
|
30
|
+
if (!retryable || attempt === 3) {
|
|
31
|
+
const error = await response.json();
|
|
32
|
+
throw new Error(`Xquik API ${response.status}: ${error.error}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
36
|
+
const delay = retryAfter
|
|
37
|
+
? parseInt(retryAfter, 10) * 1000
|
|
38
|
+
: baseDelay * Math.pow(2, attempt) + Math.random() * 1000;
|
|
39
|
+
|
|
40
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Cursor Pagination
|
|
46
|
+
|
|
47
|
+
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.
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
async function fetchAllPages(path, dataKey) {
|
|
51
|
+
const results = [];
|
|
52
|
+
let cursor;
|
|
53
|
+
|
|
54
|
+
while (true) {
|
|
55
|
+
const params = new URLSearchParams({ limit: "100" });
|
|
56
|
+
if (cursor) params.set("after", cursor);
|
|
57
|
+
|
|
58
|
+
const data = await xquikFetch(`${path}?${params}`);
|
|
59
|
+
results.push(...data[dataKey]);
|
|
60
|
+
|
|
61
|
+
if (!data.hasMore) break;
|
|
62
|
+
cursor = data.nextCursor;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Cursors are opaque strings. Never decode or construct them manually.
|
|
70
|
+
|
|
71
|
+
## Complete Extraction Workflow
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
// Step 1: Estimate cost before running
|
|
75
|
+
const estimate = await xquikFetch("/extractions/estimate", {
|
|
76
|
+
method: "POST",
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
toolType: "follower_explorer",
|
|
79
|
+
targetUsername: "elonmusk",
|
|
80
|
+
resultsLimit: 1000,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!estimate.allowed) {
|
|
85
|
+
console.log("Extraction would exceed monthly quota");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Step 2: Create extraction job
|
|
90
|
+
const job = await xquikFetch("/extractions", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
toolType: "follower_explorer",
|
|
94
|
+
targetUsername: "elonmusk",
|
|
95
|
+
resultsLimit: 1000,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Step 3: Poll until complete
|
|
100
|
+
while (job.status === "pending" || job.status === "running") {
|
|
101
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
102
|
+
job = await xquikFetch(`/extractions/${job.id}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Step 4: Retrieve paginated results (up to 1,000 per page)
|
|
106
|
+
let cursor;
|
|
107
|
+
const allResults = [];
|
|
108
|
+
|
|
109
|
+
while (true) {
|
|
110
|
+
const path = `/extractions/${job.id}${cursor ? `?after=${cursor}` : ""}`;
|
|
111
|
+
const page = await xquikFetch(path);
|
|
112
|
+
allResults.push(...page.results);
|
|
113
|
+
|
|
114
|
+
if (!page.hasMore) break;
|
|
115
|
+
cursor = page.nextCursor;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Step 5: Export as CSV/XLSX/Markdown (50,000 row limit)
|
|
119
|
+
const exportUrl = `${BASE}/extractions/${job.id}/export?format=csv`;
|
|
120
|
+
const csvResponse = await fetch(exportUrl, { headers });
|
|
121
|
+
const csvData = await csvResponse.text();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Real-Time Monitoring Setup
|
|
125
|
+
|
|
126
|
+
Complete end-to-end: create monitor, register webhook, handle events.
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
// 1. Create monitor (free)
|
|
130
|
+
const monitor = await xquikFetch("/monitors", {
|
|
131
|
+
method: "POST",
|
|
132
|
+
body: JSON.stringify({
|
|
133
|
+
username: "elonmusk",
|
|
134
|
+
eventTypes: ["tweet.new", "tweet.reply", "tweet.quote", "follower.gained"],
|
|
135
|
+
}),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// 2. Register webhook (free)
|
|
139
|
+
const webhook = await xquikFetch("/webhooks", {
|
|
140
|
+
method: "POST",
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
url: "https://your-server.com/webhook",
|
|
143
|
+
eventTypes: ["tweet.new", "tweet.reply"],
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
// IMPORTANT: Save webhook.secret. It is shown only once!
|
|
147
|
+
|
|
148
|
+
// 3. Poll events (alternative to webhooks, free)
|
|
149
|
+
const events = await xquikFetch("/events?monitorId=7&limit=50");
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Event types: `tweet.new`, `tweet.quote`, `tweet.reply`, `tweet.retweet`, `follower.gained`, `follower.lost`.
|
|
153
|
+
|
|
154
|
+
## Endpoint Guide
|
|
155
|
+
|
|
156
|
+
| Goal | Endpoint | Cost |
|
|
157
|
+
|------|----------|------|
|
|
158
|
+
| **Get a single tweet** by ID/URL | `GET /x/tweets/{id}` | 1 credit |
|
|
159
|
+
| **Get an X Article** by tweet ID | `GET /x/articles/{id}` | 7 credits |
|
|
160
|
+
| **Search tweets** by keyword/hashtag | `GET /x/tweets/search?q=...` | 1 credit/tweet |
|
|
161
|
+
| **Get a user profile** | `GET /x/users/{username}` | 1 credit |
|
|
162
|
+
| **Get user's recent tweets** | `GET /x/users/{id}/tweets` | 1 credit/tweet |
|
|
163
|
+
| **Get user's liked tweets** | `GET /x/users/{id}/likes` | 1 credit/result |
|
|
164
|
+
| **Get user's media tweets** | `GET /x/users/{id}/media` | 1 credit/result |
|
|
165
|
+
| **Get tweet favoriters** | `GET /x/tweets/{id}/favoriters` | 1 credit/result |
|
|
166
|
+
| **Get mutual followers** | `GET /x/users/{id}/followers-you-know` | 1 credit/result |
|
|
167
|
+
| **Check follow relationship** | `GET /x/followers/check?source=A&target=B` | 7 credits |
|
|
168
|
+
| **Get trending topics** | `GET /trends?woeid=1` | 3 credits |
|
|
169
|
+
| **Get radar (trending news)** | `GET /radar?source=hacker_news` | Free |
|
|
170
|
+
| **Get bookmarks** | `GET /x/bookmarks` | 1 credit/result |
|
|
171
|
+
| **Get bookmark folders** | `GET /x/bookmarks/folders` | 1 credit |
|
|
172
|
+
| **Get notifications** | `GET /x/notifications` | 1 credit/result |
|
|
173
|
+
| **Get home timeline** | `GET /x/timeline` | 1 credit/result |
|
|
174
|
+
| **Get DM history** | `GET /x/dm/{userId}/history` | 1 credit/result |
|
|
175
|
+
| **Monitor an X account** | `POST /monitors` | Free |
|
|
176
|
+
| **Poll for events** | `GET /events` | Free |
|
|
177
|
+
| **Receive events via webhook** | `POST /webhooks` | Free |
|
|
178
|
+
| **Run a giveaway draw** | `POST /draws` | 1 credit/entry |
|
|
179
|
+
| **Download tweet media** | `POST /x/media/download` | 1 credit/item |
|
|
180
|
+
| **Extract bulk data** | `POST /extractions` | 1-5 credits/result |
|
|
181
|
+
| **Check credits** | `GET /credits` | Free |
|
|
182
|
+
| **Top up credits** | `POST /credits/topup` | Free |
|
|
183
|
+
| **Compose a tweet** | `POST /compose` | Free |
|
|
184
|
+
| **Post a tweet** | `POST /x/tweets` | 10 credits |
|
|
185
|
+
| **Like / Unlike a tweet** | `POST` / `DELETE /x/tweets/{id}/like` | 10 credits |
|
|
186
|
+
| **Retweet** | `POST /x/tweets/{id}/retweet` | 10 credits |
|
|
187
|
+
| **Follow / Unfollow** | `POST` / `DELETE /x/users/{id}/follow` | 10 credits |
|
|
188
|
+
| **Send a DM** | `POST /x/dm/{userId}` | 10 credits |
|
|
189
|
+
| **Update profile** | `PATCH /x/profile` | 10 credits |
|
|
190
|
+
| **Upload media** | `POST /x/media` | 10 credits |
|
|
191
|
+
| **Community actions** | `POST /x/communities`, join/leave | 10 credits |
|
|
192
|
+
| **Telegram integration** | `POST /integrations` | Free |
|
|
193
|
+
| **Automation flows** | `POST /automations` | Free |
|
|
194
|
+
| **Support tickets** | `POST /support/tickets` | Free |
|