x-developer 2.5.6 → 2.6.2
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/.claude-plugin/marketplace.json +4 -4
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/CHANGELOG.md +55 -0
- package/README.md +201 -18
- package/commands/post.md +14 -7
- package/commands/trending.md +4 -2
- package/context7.json +3 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +9 -3
- package/scripts/check-reproducible.mjs +48 -0
- package/scripts/check-versions.mjs +12 -431
- package/scripts/release-guard/content-policy.mjs +77 -18
- package/scripts/release-guard/context.mjs +61 -0
- package/scripts/release-guard/frontmatter.mjs +3 -0
- package/scripts/release-guard/policy-checks.mjs +134 -0
- package/scripts/release-guard/policy.mjs +4 -1
- package/scripts/release-guard/repository-checks.mjs +256 -0
- package/server.json +2 -2
- package/skills/x-twitter-scraper/SKILL.md +106 -11
- package/skills/x-twitter-scraper/metadata.json +1 -1
- package/skills/x-twitter-scraper/references/api-endpoints-api-keys.md +6 -23
- package/skills/x-twitter-scraper/references/api-endpoints-drafts.md +20 -1
- package/skills/x-twitter-scraper/references/api-endpoints-draws.md +26 -0
- package/skills/x-twitter-scraper/references/api-endpoints-error-codes.md +3 -0
- package/skills/x-twitter-scraper/references/api-endpoints-events.md +3 -1
- package/skills/x-twitter-scraper/references/api-endpoints-extractions.md +41 -26
- package/skills/x-twitter-scraper/references/api-endpoints-monitors.md +43 -6
- package/skills/x-twitter-scraper/references/api-endpoints-radar.md +2 -1
- package/skills/x-twitter-scraper/references/api-endpoints-support.md +22 -0
- package/skills/x-twitter-scraper/references/api-endpoints-trends.md +9 -1
- package/skills/x-twitter-scraper/references/api-endpoints-tweet-style-cache.md +32 -1
- package/skills/x-twitter-scraper/references/api-endpoints-webhooks.md +55 -9
- package/skills/x-twitter-scraper/references/api-endpoints-x-accounts.md +12 -0
- package/skills/x-twitter-scraper/references/api-endpoints-x-api.md +51 -11
- package/skills/x-twitter-scraper/references/api-endpoints-x-media.md +10 -0
- package/skills/x-twitter-scraper/references/api-endpoints-x-write.md +85 -19
- package/skills/x-twitter-scraper/references/automate-twitter-giveaways.md +130 -0
- package/skills/x-twitter-scraper/references/best-x-api-alternative.md +198 -0
- package/skills/x-twitter-scraper/references/compare-twitter-apis.md +152 -0
- package/skills/x-twitter-scraper/references/draws.md +1 -1
- package/skills/x-twitter-scraper/references/export-twitter-followers.md +127 -0
- package/skills/x-twitter-scraper/references/extract-x-community-data.md +121 -0
- package/skills/x-twitter-scraper/references/extractions.md +74 -6
- package/skills/x-twitter-scraper/references/mcp-setup.md +14 -5
- package/skills/x-twitter-scraper/references/mcp-tools.md +14 -4
- package/skills/x-twitter-scraper/references/monitor-twitter-webhooks.md +122 -0
- package/skills/x-twitter-scraper/references/python-examples.md +39 -12
- package/skills/x-twitter-scraper/references/reliable-twitter-data-api-2026.md +241 -0
- package/skills/x-twitter-scraper/references/scrape-export-twitter-data.md +171 -0
- package/skills/x-twitter-scraper/references/track-twitter-keywords-mentions.md +148 -0
- package/skills/x-twitter-scraper/references/twitter-api-alternative-faq.md +128 -0
- package/skills/x-twitter-scraper/references/twitter-api-without-x-account.md +111 -0
- package/skills/x-twitter-scraper/references/twitter-data-pipeline.md +136 -0
- package/skills/x-twitter-scraper/references/twitter-scraper-api-guide.md +229 -0
- package/skills/x-twitter-scraper/references/types-events.md +5 -25
- package/skills/x-twitter-scraper/references/types-extractions.md +49 -2
- package/skills/x-twitter-scraper/references/types-mcp-output-schemas.md +10 -19
- package/skills/x-twitter-scraper/references/types-monitors.md +30 -1
- package/skills/x-twitter-scraper/references/types-radar.md +7 -0
- package/skills/x-twitter-scraper/references/types-rest-api-vs-mcp-field-naming.md +8 -12
- package/skills/x-twitter-scraper/references/types-trends.md +3 -0
- package/skills/x-twitter-scraper/references/types-webhooks.md +10 -2
- package/skills/x-twitter-scraper/references/types-x-api.md +93 -12
- package/skills/x-twitter-scraper/references/types-x-articles.md +27 -12
- package/skills/x-twitter-scraper/references/types-x-write.md +46 -7
- package/skills/x-twitter-scraper/references/types.md +0 -17
- package/skills/x-twitter-scraper/references/webhooks.md +123 -35
- package/skills/x-twitter-scraper/references/workflows.md +27 -9
- package/skills/x-twitter-scraper/skill-card.md +2 -2
- package/skills/x-twitter-scraper/skillspector-report.md +5 -2
- package/skills/xquik-social-research/SKILL.md +1 -0
- package/start.sh +3 -0
- package/stub-server.mjs +91 -50
- package/task-guides/check-mutuals.md +1 -1
- package/task-guides/clean-followers.md +1 -1
- package/task-guides/export-tweets-csv.md +14 -5
- package/task-guides/extract-followers.md +2 -2
- package/task-guides/find-bangers.md +4 -2
- package/task-guides/find-influencers.md +7 -4
- package/task-guides/find-viral-tweets.md +1 -1
- package/task-guides/follow-unfollow.md +9 -4
- package/task-guides/for-you-feed.md +1 -1
- package/task-guides/going-viral.md +1 -1
- package/task-guides/grow-followers.md +4 -2
- package/task-guides/monitor-accounts.md +8 -9
- package/task-guides/optimize-tweets.md +1 -1
- package/task-guides/post-tweets.md +11 -7
- package/task-guides/run-giveaway.md +1 -1
- package/task-guides/search-tweets.md +16 -8
- package/task-guides/send-dms.md +16 -7
- package/task-guides/top-replies.md +25 -9
- package/task-guides/track-competitors.md +3 -3
- package/task-guides/track-hashtags.md +7 -8
- package/task-guides/track-mentions.md +10 -9
- package/task-guides/trending-news.md +13 -8
- package/task-guides/tweet-analytics.md +2 -2
- package/task-guides/tweet-ideas.md +1 -1
- package/task-guides/tweet-replies.md +63 -14
- package/task-guides/tweet-style.md +1 -1
- package/task-guides/tweet-webhooks.md +16 -12
- package/task-guides/update-x-profile.md +8 -5
- package/task-guides/user-tweets.md +20 -8
- package/task-guides/who-liked.md +1 -1
- package/task-guides/who-quoted.md +1 -1
- package/task-guides/who-retweeted.md +1 -1
- package/task-guides/write-threads.md +1 -1
- package/task-guides/write-tweets.md +1 -1
- package/task-guides/x-articles.md +10 -5
- package/task-guides/x-bookmarks.md +1 -1
- package/task-guides/x-communities.md +1 -1
- package/task-guides/x-lists.md +1 -1
- package/task-guides/x-spaces.md +1 -1
- package/task-guides/x-trends.md +6 -6
- package/skills/x-twitter-scraper/references/types-mcp-add-webhook.md +0 -14
- package/skills/x-twitter-scraper/references/types-mcp-check-follow.md +0 -10
- package/skills/x-twitter-scraper/references/types-mcp-compose-tweet.md +0 -31
- package/skills/x-twitter-scraper/references/types-mcp-estimate-extraction.md +0 -15
- package/skills/x-twitter-scraper/references/types-mcp-get-account.md +0 -26
- package/skills/x-twitter-scraper/references/types-mcp-get-draw.md +0 -30
- package/skills/x-twitter-scraper/references/types-mcp-get-events.md +0 -19
- package/skills/x-twitter-scraper/references/types-mcp-get-trends.md +0 -16
- package/skills/x-twitter-scraper/references/types-mcp-get-user-info.md +0 -16
- package/skills/x-twitter-scraper/references/types-mcp-list-monitors.md +0 -15
- package/skills/x-twitter-scraper/references/types-mcp-lookup-tweet.md +0 -25
- package/skills/x-twitter-scraper/references/types-mcp-refine-tweet.md +0 -13
- package/skills/x-twitter-scraper/references/types-mcp-run-draw.md +0 -18
- package/skills/x-twitter-scraper/references/types-mcp-run-extraction.md +0 -12
- package/skills/x-twitter-scraper/references/types-mcp-score-tweet.md +0 -16
- package/skills/x-twitter-scraper/references/types-mcp-search-tweets.md +0 -17
- package/skills/x-twitter-scraper/references/types-mcp-test-webhook.md +0 -11
|
@@ -25,8 +25,12 @@ Every delivery is a `POST` request to your URL with a JSON body:
|
|
|
25
25
|
|
|
26
26
|
```json
|
|
27
27
|
{
|
|
28
|
+
"schemaVersion": 1,
|
|
29
|
+
"streamEventId": "9010",
|
|
30
|
+
"deliveryId": "334",
|
|
28
31
|
"eventType": "tweet.new",
|
|
29
32
|
"username": "elonmusk",
|
|
33
|
+
"occurredAt": "2026-02-24T16:45:00.000Z",
|
|
30
34
|
"data": {
|
|
31
35
|
"tweetId": "1893556789012345678",
|
|
32
36
|
"text": "Hello world",
|
|
@@ -37,7 +41,16 @@ Every delivery is a `POST` request to your URL with a JSON body:
|
|
|
37
41
|
|
|
38
42
|
## Signature Verification
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
Each request contains `X-Xquik-Timestamp`, `X-Xquik-Nonce`, and
|
|
45
|
+
`X-Xquik-Signature`. The signature is `sha256=` plus HMAC-SHA256 over:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
<timestamp>.<nonce>.<raw JSON body>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Reject timestamps outside a 5-minute window. Reject reused nonces within that
|
|
52
|
+
window. Compare signatures in constant time before parsing JSON.
|
|
53
|
+
Use an atomic shared nonce store in multi-instance deployments.
|
|
41
54
|
|
|
42
55
|
### Node.js (Standard Library)
|
|
43
56
|
|
|
@@ -47,11 +60,25 @@ import { createServer } from "node:http";
|
|
|
47
60
|
|
|
48
61
|
// This is the per-webhook secret from the POST /webhooks response, not a Xquik account credential
|
|
49
62
|
const WEBHOOK_SECRET = process.env.XQUIK_WEBHOOK_SECRET;
|
|
63
|
+
const recentNonces = new Map();
|
|
50
64
|
|
|
51
|
-
function
|
|
52
|
-
|
|
65
|
+
function claimNonce(nonce) {
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
for (const [value, expiresAt] of recentNonces) {
|
|
68
|
+
if (expiresAt <= now) recentNonces.delete(value);
|
|
69
|
+
}
|
|
70
|
+
if (recentNonces.has(nonce)) return false;
|
|
71
|
+
recentNonces.set(nonce, now + 5 * 60 * 1000);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
53
74
|
|
|
54
|
-
|
|
75
|
+
function verifySignature(payload, signature, timestamp, nonce, secret) {
|
|
76
|
+
if (![signature, timestamp, nonce, secret].every((value) => typeof value === "string")) return false;
|
|
77
|
+
if (!/^\d+$/.test(timestamp) || !/^[0-9a-f]{32}$/.test(nonce)) return false;
|
|
78
|
+
if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) return false;
|
|
79
|
+
|
|
80
|
+
const input = `${timestamp}.${nonce}.${payload}`;
|
|
81
|
+
const expected = "sha256=" + createHmac("sha256", secret).update(input).digest("hex");
|
|
55
82
|
const expectedBuffer = Buffer.from(expected, "utf8");
|
|
56
83
|
const signatureBuffer = Buffer.from(signature, "utf8");
|
|
57
84
|
|
|
@@ -73,26 +100,25 @@ const server = createServer((req, res) => {
|
|
|
73
100
|
req.on("end", () => {
|
|
74
101
|
const payload = Buffer.concat(chunks).toString("utf8");
|
|
75
102
|
const signature = req.headers["x-xquik-signature"];
|
|
103
|
+
const timestamp = req.headers["x-xquik-timestamp"];
|
|
104
|
+
const nonce = req.headers["x-xquik-nonce"];
|
|
76
105
|
|
|
77
|
-
if (
|
|
106
|
+
if (
|
|
107
|
+
!verifySignature(payload, signature, timestamp, nonce, WEBHOOK_SECRET) ||
|
|
108
|
+
!claimNonce(nonce)
|
|
109
|
+
) {
|
|
78
110
|
res.writeHead(401).end("Invalid signature");
|
|
79
111
|
return;
|
|
80
112
|
}
|
|
81
113
|
|
|
82
114
|
const event = JSON.parse(payload);
|
|
83
115
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
break;
|
|
88
|
-
case "tweet.reply":
|
|
89
|
-
console.log(`Reply from @${event.username}: ${event.data.text}`);
|
|
90
|
-
break;
|
|
91
|
-
case "tweet.retweet":
|
|
92
|
-
console.log(`@${event.username} retweeted`);
|
|
93
|
-
break;
|
|
116
|
+
if (!["tweet.new", "tweet.reply", "tweet.retweet"].includes(event.eventType)) {
|
|
117
|
+
res.writeHead(400).end("Unsupported event type");
|
|
118
|
+
return;
|
|
94
119
|
}
|
|
95
120
|
|
|
121
|
+
console.log("Accepted verified Xquik webhook");
|
|
96
122
|
res.writeHead(200).end("OK");
|
|
97
123
|
});
|
|
98
124
|
});
|
|
@@ -106,6 +132,8 @@ server.listen(3000);
|
|
|
106
132
|
import hmac
|
|
107
133
|
import hashlib
|
|
108
134
|
import json
|
|
135
|
+
import re
|
|
136
|
+
import time
|
|
109
137
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
110
138
|
|
|
111
139
|
def load_secret(name: str) -> str:
|
|
@@ -114,20 +142,36 @@ def load_secret(name: str) -> str:
|
|
|
114
142
|
|
|
115
143
|
# Per-webhook secret from POST /webhooks response, not a Xquik account credential
|
|
116
144
|
WEBHOOK_SECRET = load_secret("XQUIK_WEBHOOK_SECRET")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
145
|
+
RECENT_NONCES: dict[str, int] = {}
|
|
146
|
+
|
|
147
|
+
def claim_nonce(nonce: str) -> bool:
|
|
148
|
+
now = int(time.time() * 1000)
|
|
149
|
+
for value, expires_at in list(RECENT_NONCES.items()):
|
|
150
|
+
if expires_at <= now:
|
|
151
|
+
RECENT_NONCES.pop(value, None)
|
|
152
|
+
if nonce in RECENT_NONCES:
|
|
153
|
+
return False
|
|
154
|
+
RECENT_NONCES[nonce] = now + 5 * 60 * 1000
|
|
155
|
+
return True
|
|
156
|
+
|
|
157
|
+
def verify_signature(payload: bytes, signature: str, timestamp: str, nonce: str, secret: str) -> bool:
|
|
158
|
+
if not timestamp.isdigit() or not re.fullmatch(r"[0-9a-f]{32}", nonce):
|
|
159
|
+
return False
|
|
160
|
+
if abs(int(time.time() * 1000) - int(timestamp)) > 5 * 60 * 1000:
|
|
161
|
+
return False
|
|
162
|
+
signing_input = timestamp.encode() + b"." + nonce.encode() + b"." + payload
|
|
163
|
+
expected = "sha256=" + hmac.new(secret.encode(), signing_input, hashlib.sha256).hexdigest()
|
|
122
164
|
return hmac.compare_digest(expected, signature)
|
|
123
165
|
|
|
124
166
|
class WebhookHandler(BaseHTTPRequestHandler):
|
|
125
167
|
def do_POST(self):
|
|
126
168
|
signature = self.headers.get("X-Xquik-Signature", "")
|
|
169
|
+
timestamp = self.headers.get("X-Xquik-Timestamp", "")
|
|
170
|
+
nonce = self.headers.get("X-Xquik-Nonce", "")
|
|
127
171
|
length = int(self.headers.get("Content-Length", "0"))
|
|
128
172
|
payload = self.rfile.read(length)
|
|
129
173
|
|
|
130
|
-
if not verify_signature(payload, signature, WEBHOOK_SECRET):
|
|
174
|
+
if not verify_signature(payload, signature, timestamp, nonce, WEBHOOK_SECRET) or not claim_nonce(nonce):
|
|
131
175
|
self.send_response(401)
|
|
132
176
|
self.end_headers()
|
|
133
177
|
self.wfile.write(b"Invalid signature")
|
|
@@ -135,9 +179,13 @@ class WebhookHandler(BaseHTTPRequestHandler):
|
|
|
135
179
|
|
|
136
180
|
event = json.loads(payload)
|
|
137
181
|
|
|
138
|
-
if event
|
|
139
|
-
|
|
182
|
+
if event.get("eventType") not in {"tweet.new", "tweet.reply", "tweet.retweet"}:
|
|
183
|
+
self.send_response(400)
|
|
184
|
+
self.end_headers()
|
|
185
|
+
self.wfile.write(b"Unsupported event type")
|
|
186
|
+
return
|
|
140
187
|
|
|
188
|
+
print("Accepted verified Xquik webhook")
|
|
141
189
|
self.send_response(200)
|
|
142
190
|
self.end_headers()
|
|
143
191
|
self.wfile.write(b"OK")
|
|
@@ -159,13 +207,39 @@ import (
|
|
|
159
207
|
"io"
|
|
160
208
|
"net/http"
|
|
161
209
|
"os"
|
|
210
|
+
"regexp"
|
|
211
|
+
"strconv"
|
|
212
|
+
"sync"
|
|
213
|
+
"time"
|
|
162
214
|
)
|
|
163
215
|
|
|
164
216
|
// Per-webhook secret from POST /webhooks response, not a Xquik account credential
|
|
165
217
|
var webhookSecret = os.Getenv("XQUIK_WEBHOOK_SECRET")
|
|
218
|
+
var recentNonces sync.Map
|
|
219
|
+
|
|
220
|
+
func claimNonce(nonce string) bool {
|
|
221
|
+
now := time.Now().UnixMilli()
|
|
222
|
+
recentNonces.Range(func(key, value any) bool {
|
|
223
|
+
if value.(int64) <= now {
|
|
224
|
+
recentNonces.Delete(key)
|
|
225
|
+
}
|
|
226
|
+
return true
|
|
227
|
+
})
|
|
228
|
+
_, replayed := recentNonces.LoadOrStore(nonce, now+5*60*1000)
|
|
229
|
+
return !replayed
|
|
230
|
+
}
|
|
166
231
|
|
|
167
|
-
func verifySignature(payload []byte, signature, secret string) bool {
|
|
232
|
+
func verifySignature(payload []byte, signature, timestamp, nonce, secret string) bool {
|
|
233
|
+
signedAt, err := strconv.ParseInt(timestamp, 10, 64)
|
|
234
|
+
if err != nil || !regexp.MustCompile(`^[0-9a-f]{32}$`).MatchString(nonce) {
|
|
235
|
+
return false
|
|
236
|
+
}
|
|
237
|
+
age := time.Now().UnixMilli() - signedAt
|
|
238
|
+
if age < -5*60*1000 || age > 5*60*1000 {
|
|
239
|
+
return false
|
|
240
|
+
}
|
|
168
241
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
242
|
+
mac.Write([]byte(timestamp + "." + nonce + "."))
|
|
169
243
|
mac.Write(payload)
|
|
170
244
|
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
|
171
245
|
return hmac.Equal([]byte(expected), []byte(signature))
|
|
@@ -179,8 +253,10 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
179
253
|
}
|
|
180
254
|
|
|
181
255
|
signature := r.Header.Get("X-Xquik-Signature")
|
|
256
|
+
timestamp := r.Header.Get("X-Xquik-Timestamp")
|
|
257
|
+
nonce := r.Header.Get("X-Xquik-Nonce")
|
|
182
258
|
|
|
183
|
-
if !verifySignature(payload, signature, webhookSecret) {
|
|
259
|
+
if !verifySignature(payload, signature, timestamp, nonce, webhookSecret) || !claimNonce(nonce) {
|
|
184
260
|
http.Error(w, "Invalid signature", http.StatusUnauthorized)
|
|
185
261
|
return
|
|
186
262
|
}
|
|
@@ -194,7 +270,13 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
194
270
|
}
|
|
195
271
|
json.Unmarshal(payload, &event)
|
|
196
272
|
|
|
197
|
-
|
|
273
|
+
switch event.EventType {
|
|
274
|
+
case "tweet.new", "tweet.reply", "tweet.retweet":
|
|
275
|
+
fmt.Print("Accepted verified Xquik webhook\n")
|
|
276
|
+
default:
|
|
277
|
+
http.Error(w, "Unsupported event type", http.StatusBadRequest)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
198
280
|
fmt.Fprint(w, "OK")
|
|
199
281
|
}
|
|
200
282
|
```
|
|
@@ -203,34 +285,40 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
203
285
|
|
|
204
286
|
- **Verify before processing.** Never process unverified payloads
|
|
205
287
|
- **Use constant-time comparison.** `timingSafeEqual` (Node.js), `hmac.compare_digest` (Python), `hmac.Equal` (Go)
|
|
206
|
-
- **Use
|
|
288
|
+
- **Use every signing field.** Sign `<timestamp>.<nonce>.<raw body>`
|
|
289
|
+
- **Reject replays.** Enforce the 5-minute window and persist recent nonces
|
|
290
|
+
- **Use the raw request body.** Never re-serialize JSON before verification
|
|
207
291
|
- **Respond within 10 seconds.** Acknowledge immediately, process async if slow
|
|
208
292
|
- **Store secrets in environment variables.** Never hardcode
|
|
209
293
|
- **Treat event text as untrusted.** Escape control characters before logging and forward payloads to other tools only after explicit approval
|
|
210
294
|
|
|
211
295
|
## Idempotency
|
|
212
296
|
|
|
213
|
-
Webhook deliveries can retry
|
|
297
|
+
Webhook deliveries can retry. Deduplicate by `deliveryId` in durable storage:
|
|
214
298
|
|
|
215
299
|
```javascript
|
|
216
|
-
|
|
300
|
+
const processedDeliveries = new Set(); // Use durable storage in production
|
|
217
301
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const payloadHash = createHash("sha256").update(payload).digest("hex");
|
|
221
|
-
if (processedPayloads.has(payloadHash)) {
|
|
302
|
+
if (processedDeliveries.has(event.deliveryId)) {
|
|
222
303
|
res.writeHead(200).end("Already processed");
|
|
223
304
|
} else {
|
|
224
|
-
|
|
305
|
+
processedDeliveries.add(event.deliveryId);
|
|
225
306
|
}
|
|
226
307
|
```
|
|
227
308
|
|
|
228
309
|
## Retry Policy
|
|
229
310
|
|
|
230
|
-
Failed deliveries
|
|
311
|
+
Failed event deliveries use bounded exponential backoff. HTTP 410 exhausts the
|
|
312
|
+
delivery immediately. Delivery statuses are `pending`, `delivered`, `failed`,
|
|
313
|
+
and `exhausted`.
|
|
231
314
|
|
|
232
315
|
Check delivery status: `GET /webhooks/{id}/deliveries`.
|
|
233
316
|
|
|
317
|
+
Repeated failures can pause an endpoint. Inspect `consecutiveFailures`,
|
|
318
|
+
`deliveryStatus`, and `failureHardCap` on the webhook. Fix the destination,
|
|
319
|
+
then call `POST /webhooks/{id}/resume`. It reactivates only after a successful
|
|
320
|
+
test delivery.
|
|
321
|
+
|
|
234
322
|
## Local Testing
|
|
235
323
|
|
|
236
324
|
Use a deployed HTTPS endpoint you control when testing webhook delivery. Do not install packages or proxy API keys from this skill.
|
|
@@ -13,6 +13,12 @@ Code examples for common integration patterns.
|
|
|
13
13
|
|
|
14
14
|
## Authentication
|
|
15
15
|
|
|
16
|
+
> **External transmission:** These examples send credentials, parameters, and
|
|
17
|
+
> returned data to and from `xquik.com`. Keep the key in a secret store. Get
|
|
18
|
+
> explicit approval before private reads, writes, exports, persistent resources,
|
|
19
|
+
> webhooks, or metered jobs. Never forward private results without separate
|
|
20
|
+
> approval.
|
|
21
|
+
|
|
16
22
|
```javascript
|
|
17
23
|
const apiKey = process.env.XQUIK_API_KEY;
|
|
18
24
|
if (!apiKey) throw new Error("Set XQUIK_API_KEY first.");
|
|
@@ -23,11 +29,14 @@ const headers = { "x-api-key": apiKey, "Content-Type": "application/json" };
|
|
|
23
29
|
|
|
24
30
|
## Retry with Exponential Backoff
|
|
25
31
|
|
|
26
|
-
Retry only `429` and `5xx`. Never retry
|
|
32
|
+
Retry only idempotent requests after `429` and `5xx`. Never automatically retry
|
|
33
|
+
`POST`, `PATCH`, or `DELETE`. Max 3 retries:
|
|
27
34
|
|
|
28
35
|
```javascript
|
|
29
36
|
async function xquikFetch(path, options = {}) {
|
|
30
37
|
const baseDelay = 1000;
|
|
38
|
+
const method = (options.method || "GET").toUpperCase();
|
|
39
|
+
const retrySafe = ["GET", "HEAD", "OPTIONS"].includes(method);
|
|
31
40
|
|
|
32
41
|
for (let attempt = 0; attempt <= 3; attempt++) {
|
|
33
42
|
const response = await fetch(`${BASE}${path}`, {
|
|
@@ -37,7 +46,7 @@ async function xquikFetch(path, options = {}) {
|
|
|
37
46
|
|
|
38
47
|
if (response.ok) return response.json();
|
|
39
48
|
|
|
40
|
-
const retryable = response.status === 429 || response.status >= 500;
|
|
49
|
+
const retryable = retrySafe && (response.status === 429 || response.status >= 500);
|
|
41
50
|
if (!retryable || attempt === 3) {
|
|
42
51
|
const error = await response.json();
|
|
43
52
|
throw new Error(`Xquik API ${response.status}: ${error.error}`);
|
|
@@ -55,7 +64,9 @@ async function xquikFetch(path, options = {}) {
|
|
|
55
64
|
|
|
56
65
|
## Cursor Pagination
|
|
57
66
|
|
|
58
|
-
Events, draws, extractions, and extraction results use cursor-based pagination.
|
|
67
|
+
Events, draws, extractions, and extraction results use cursor-based pagination.
|
|
68
|
+
When more results exist, the response includes `hasMore: true` and a
|
|
69
|
+
`nextCursor` string. Pass it as `cursor`. Radar alone uses `after`.
|
|
59
70
|
|
|
60
71
|
```javascript
|
|
61
72
|
async function fetchAllPages(path, dataKey) {
|
|
@@ -64,7 +75,7 @@ async function fetchAllPages(path, dataKey) {
|
|
|
64
75
|
|
|
65
76
|
while (true) {
|
|
66
77
|
const params = new URLSearchParams({ limit: "100" });
|
|
67
|
-
if (cursor) params.set("
|
|
78
|
+
if (cursor) params.set("cursor", cursor);
|
|
68
79
|
|
|
69
80
|
const data = await xquikFetch(`${path}?${params}`);
|
|
70
81
|
results.push(...data[dataKey]);
|
|
@@ -82,6 +93,10 @@ Cursors are opaque strings. Never decode or construct them manually.
|
|
|
82
93
|
## Complete Extraction Workflow
|
|
83
94
|
|
|
84
95
|
```javascript
|
|
96
|
+
function requireExplicitApproval(scope) {
|
|
97
|
+
throw new Error(`Approval required for ${scope}. Implement the approval gate first.`);
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
// Step 1: Estimate usage before running
|
|
86
101
|
const estimate = await xquikFetch("/extractions/estimate", {
|
|
87
102
|
method: "POST",
|
|
@@ -97,7 +112,8 @@ if (!estimate.allowed) {
|
|
|
97
112
|
return;
|
|
98
113
|
}
|
|
99
114
|
|
|
100
|
-
// Step 2: Create extraction job
|
|
115
|
+
// Step 2: Create extraction job after approving this exact bounded request
|
|
116
|
+
requireExplicitApproval("the bounded extraction job, usage, recipients, and retention");
|
|
101
117
|
let job = await xquikFetch("/extractions", {
|
|
102
118
|
method: "POST",
|
|
103
119
|
body: JSON.stringify({
|
|
@@ -118,7 +134,7 @@ let cursor;
|
|
|
118
134
|
const allResults = [];
|
|
119
135
|
|
|
120
136
|
while (true) {
|
|
121
|
-
const path = `/extractions/${job.id}${cursor ? `?
|
|
137
|
+
const path = `/extractions/${job.id}${cursor ? `?cursor=${cursor}` : ""}`;
|
|
122
138
|
const page = await xquikFetch(path);
|
|
123
139
|
allResults.push(...page.results);
|
|
124
140
|
|
|
@@ -126,7 +142,8 @@ while (true) {
|
|
|
126
142
|
cursor = page.nextCursor;
|
|
127
143
|
}
|
|
128
144
|
|
|
129
|
-
// Step 5: Export
|
|
145
|
+
// Step 5: Export only after reviewing a bounded preview and approving transmission
|
|
146
|
+
requireExplicitApproval("the fixed export scope, audience, storage, and retention");
|
|
130
147
|
const exportUrl = `${BASE}/extractions/${job.id}/export?format=csv`;
|
|
131
148
|
const csvResponse = await fetch(exportUrl, { headers });
|
|
132
149
|
const csvData = await csvResponse.text();
|
|
@@ -160,7 +177,8 @@ const webhook = await xquikFetch("/webhooks", {
|
|
|
160
177
|
const events = await xquikFetch("/events?monitorId=7&limit=50");
|
|
161
178
|
```
|
|
162
179
|
|
|
163
|
-
|
|
180
|
+
Monitor event types include `tweet.new`, `tweet.quote`, `tweet.reply`, and
|
|
181
|
+
`tweet.retweet`. Test deliveries use `webhook.test`; do not subscribe to it.
|
|
164
182
|
|
|
165
183
|
## Endpoint Guide
|
|
166
184
|
|
|
@@ -182,7 +200,7 @@ Event types: `tweet.new`, `tweet.quote`, `tweet.reply`, `tweet.retweet`, `webhoo
|
|
|
182
200
|
| **Get bookmark folders** | `GET /x/bookmarks/folders` | Metered |
|
|
183
201
|
| **Get notifications** | `GET /x/notifications` | Metered per result |
|
|
184
202
|
| **Get home timeline** | `GET /x/timeline` | Metered per result |
|
|
185
|
-
| **Get DM history** | `GET /x/dm/{userId}/history` |
|
|
203
|
+
| **Get DM history** | `GET /x/dm/{userId}/history?account={username}` | Private; approve exact account and block ambiguity |
|
|
186
204
|
| **Monitor an X account** | `POST /monitors` | Active monitors are metered hourly |
|
|
187
205
|
| **Poll for events** | `GET /events` | Included |
|
|
188
206
|
| **Receive events via webhook** | `POST /webhooks` | Included; confirmation required for destination URL |
|
|
@@ -53,7 +53,7 @@ Mitigation: Verify unfamiliar endpoint details against `https://docs.xquik.com`
|
|
|
53
53
|
- NVIDIA signing guidance: `https://docs.nvidia.com/skills/signing-agent-skills`
|
|
54
54
|
- NVIDIA skill card guidance: `https://docs.nvidia.com/skills/skill-cards`
|
|
55
55
|
- NVIDIA release checklist: `https://docs.nvidia.com/skills/release-checklist`
|
|
56
|
-
- Scan evidence: `skillspector-report.md` records a static SkillSpector v2.3.7 scan from 2026-07-
|
|
56
|
+
- Scan evidence: `skillspector-report.md` records a static SkillSpector v2.3.7 scan from 2026-07-31 with 0 findings. Refresh it after each skill directory change.
|
|
57
57
|
- Signing evidence: pending `skill.oms.sig` for signed release artifacts.
|
|
58
58
|
- Evaluation evidence: pending Tier-3 evaluation data and `BENCHMARK.md` for NVIDIA-Verified release.
|
|
59
59
|
|
|
@@ -69,7 +69,7 @@ Other properties: The skill performs no shell execution, no local file access, n
|
|
|
69
69
|
|
|
70
70
|
## Skill Version
|
|
71
71
|
|
|
72
|
-
2.
|
|
72
|
+
2.6.2
|
|
73
73
|
|
|
74
74
|
## Ethical Considerations
|
|
75
75
|
|
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
**Skill:** x-twitter-scraper
|
|
4
4
|
**Source:** `skills/x-twitter-scraper`
|
|
5
|
-
**Scanned:** 2026-07-
|
|
5
|
+
**Scanned:** 2026-07-31 15:11:36 UTC
|
|
6
6
|
|
|
7
7
|
## Static Scan
|
|
8
8
|
|
|
9
9
|
- Scanner: SkillSpector v2.3.7
|
|
10
10
|
- Mode: static analysis only (`--no-llm`)
|
|
11
|
-
- Components scanned:
|
|
11
|
+
- Components scanned: 88
|
|
12
12
|
- Executable scripts: no
|
|
13
|
+
- Risk score: 0/100
|
|
14
|
+
- Severity: low
|
|
15
|
+
- Recommendation: safe
|
|
13
16
|
- Findings: 0
|
|
14
17
|
|
|
15
18
|
No security issues were detected by the static scan.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: xquik-social-research
|
|
3
3
|
description: Research public X data with Xquik. Use for tweet search, tweet lookup, user discovery, profile timelines, threads, followers, trends, exports, monitoring plans, or MCP setup. Keep public reads bounded. Require explicit approval before private reads, writes, persistent resources, or bulk jobs. Not affiliated with X Corp.
|
|
4
|
+
license: MIT
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Xquik Social Research
|
package/start.sh
CHANGED
package/stub-server.mjs
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// SPDX-FileCopyrightText: 2026 Xquik Contributors
|
|
4
|
+
// SPDX-License-Identifier: MIT
|
|
5
|
+
|
|
3
6
|
// Minimal stdio MCP server stub for package verification.
|
|
4
7
|
// Exposes the public tool shape used by registry checks.
|
|
5
8
|
// For live usage, connect to: https://xquik.com/mcp
|
|
6
9
|
|
|
7
10
|
import { createInterface } from "node:readline";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
8
12
|
|
|
9
13
|
const SERVER_INFO = {
|
|
10
14
|
name: "xquik",
|
|
11
|
-
version: "2.
|
|
15
|
+
version: "2.6.2",
|
|
12
16
|
};
|
|
13
17
|
|
|
14
18
|
const CAPABILITIES = {
|
|
15
19
|
tools: { listChanged: false },
|
|
16
20
|
};
|
|
17
21
|
|
|
22
|
+
const MODERN_PROTOCOL_VERSION = "2026-07-28";
|
|
23
|
+
const LEGACY_PROTOCOL_VERSION = "2025-11-25";
|
|
24
|
+
const CACHE_TTL_MS = 300_000;
|
|
18
25
|
const MAX_LINE_LENGTH = 64 * 1024;
|
|
19
26
|
const JSONRPC = "2.0";
|
|
20
27
|
const LIVE_SERVER_MESSAGE =
|
|
@@ -42,7 +49,7 @@ const TOOLS = [
|
|
|
42
49
|
{
|
|
43
50
|
name: "explore",
|
|
44
51
|
description: description([
|
|
45
|
-
"Live Xquik tool: search the
|
|
52
|
+
"Live Xquik tool: search the 120-route API catalog before calling 'xquik'. This package stub returns setup guidance only.",
|
|
46
53
|
"",
|
|
47
54
|
"## When to use",
|
|
48
55
|
"- Use 'explore' FIRST to find the right endpoint path, parameters, and response shape before calling 'xquik'.",
|
|
@@ -56,7 +63,7 @@ const TOOLS = [
|
|
|
56
63
|
"## Behavior",
|
|
57
64
|
"- Live server: read-only, idempotent catalog search with no API request.",
|
|
58
65
|
"- Package stub: makes no network call and returns live setup guidance.",
|
|
59
|
-
"- The live catalog has
|
|
66
|
+
"- The live catalog has 120 routes. Of these, 119 support JSON or text.",
|
|
60
67
|
"- Each EndpointInfo contains method, path, summary, category, free, parameters, and responseShape fields.",
|
|
61
68
|
"",
|
|
62
69
|
"## Input format",
|
|
@@ -81,7 +88,7 @@ const TOOLS = [
|
|
|
81
88
|
{
|
|
82
89
|
name: "xquik",
|
|
83
90
|
description: description([
|
|
84
|
-
"Live Xquik tool: send confirmed requests across
|
|
91
|
+
"Live Xquik tool: send confirmed requests across 120 catalog routes. This package stub returns setup guidance only.",
|
|
85
92
|
"",
|
|
86
93
|
"## When to use",
|
|
87
94
|
"- Use after calling 'explore' to discover the endpoint path and parameters.",
|
|
@@ -96,7 +103,7 @@ const TOOLS = [
|
|
|
96
103
|
"- Live server: processes `xquik.request(path, options?)` inside a bounded sandbox.",
|
|
97
104
|
"- Package stub: makes no API request and returns live setup guidance.",
|
|
98
105
|
"- The live tool has no filesystem or arbitrary network access.",
|
|
99
|
-
"-
|
|
106
|
+
"- 119 catalog routes support JSON or text. Binary support downloads use REST.",
|
|
100
107
|
"- Mutating operations require prior user confirmation and can return durable actions.",
|
|
101
108
|
"- Pagination responses include `has_more` and `next_cursor`. Pass `cursor` for the next page.",
|
|
102
109
|
"- Some operations modify X or Xquik resources. Show the exact payload, target, and usage estimate before calling them.",
|
|
@@ -127,20 +134,21 @@ const TOOLS = [
|
|
|
127
134
|
},
|
|
128
135
|
];
|
|
129
136
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
137
|
+
const DISCOVERY_RESULT = {
|
|
138
|
+
_meta: {
|
|
139
|
+
"io.modelcontextprotocol/serverInfo": SERVER_INFO,
|
|
140
|
+
},
|
|
141
|
+
cacheScope: "private",
|
|
142
|
+
capabilities: CAPABILITIES,
|
|
143
|
+
supportedVersions: [MODERN_PROTOCOL_VERSION],
|
|
144
|
+
ttlMs: CACHE_TTL_MS,
|
|
145
|
+
};
|
|
140
146
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
const TOOL_LIST_RESULT = {
|
|
148
|
+
cacheScope: "private",
|
|
149
|
+
tools: TOOLS,
|
|
150
|
+
ttlMs: CACHE_TTL_MS,
|
|
151
|
+
};
|
|
144
152
|
|
|
145
153
|
function isObject(value) {
|
|
146
154
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -150,48 +158,63 @@ function isKnownTool(name) {
|
|
|
150
158
|
return TOOLS.some((tool) => tool.name === name);
|
|
151
159
|
}
|
|
152
160
|
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
}
|
|
161
|
+
export function createMessageHandler(writeLine) {
|
|
162
|
+
function send(msg) {
|
|
163
|
+
writeLine(`${JSON.stringify(msg)}\n`);
|
|
164
|
+
}
|
|
158
165
|
|
|
159
|
-
function
|
|
160
|
-
|
|
166
|
+
function sendResult(id, result) {
|
|
167
|
+
send({ jsonrpc: JSONRPC, id, result });
|
|
168
|
+
}
|
|
161
169
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
protocolVersion: "2024-11-05",
|
|
166
|
-
serverInfo: SERVER_INFO,
|
|
167
|
-
capabilities: CAPABILITIES,
|
|
168
|
-
});
|
|
170
|
+
function sendError(id, code, message) {
|
|
171
|
+
send({ jsonrpc: JSONRPC, id, error: { code, message } });
|
|
172
|
+
}
|
|
169
173
|
|
|
170
|
-
|
|
171
|
-
|
|
174
|
+
function sendStubToolResult(id) {
|
|
175
|
+
sendResult(id, {
|
|
176
|
+
content: [{ type: "text", text: LIVE_SERVER_MESSAGE }],
|
|
177
|
+
});
|
|
178
|
+
}
|
|
172
179
|
|
|
173
|
-
|
|
174
|
-
|
|
180
|
+
return function handleMessage(msg) {
|
|
181
|
+
const { id, method, params } = msg;
|
|
182
|
+
if (id === undefined) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
175
185
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
186
|
+
switch (method) {
|
|
187
|
+
case "server/discover":
|
|
188
|
+
return sendResult(id, DISCOVERY_RESULT);
|
|
189
|
+
|
|
190
|
+
case "initialize":
|
|
191
|
+
return sendResult(id, {
|
|
192
|
+
protocolVersion: LEGACY_PROTOCOL_VERSION,
|
|
193
|
+
serverInfo: SERVER_INFO,
|
|
194
|
+
capabilities: CAPABILITIES,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
case "tools/list":
|
|
198
|
+
return sendResult(id, TOOL_LIST_RESULT);
|
|
199
|
+
|
|
200
|
+
case "tools/call": {
|
|
201
|
+
const toolName = params?.name;
|
|
202
|
+
if (isKnownTool(toolName)) {
|
|
203
|
+
return sendStubToolResult(id);
|
|
204
|
+
}
|
|
205
|
+
return sendError(id, -32601, `Unknown tool: ${toolName}`);
|
|
180
206
|
}
|
|
181
|
-
return sendError(id, -32601, `Unknown tool: ${toolName}`);
|
|
182
|
-
}
|
|
183
207
|
|
|
184
|
-
|
|
185
|
-
|
|
208
|
+
case "ping":
|
|
209
|
+
return sendResult(id, {});
|
|
186
210
|
|
|
187
|
-
|
|
188
|
-
if (id !== undefined) {
|
|
211
|
+
default:
|
|
189
212
|
return sendError(id, -32601, `Method not found: ${method}`);
|
|
190
|
-
|
|
191
|
-
}
|
|
213
|
+
}
|
|
214
|
+
};
|
|
192
215
|
}
|
|
193
216
|
|
|
194
|
-
|
|
217
|
+
export function processLine(line, handleMessage) {
|
|
195
218
|
if (line.length > MAX_LINE_LENGTH) {
|
|
196
219
|
return;
|
|
197
220
|
}
|
|
@@ -204,4 +227,22 @@ rl.on("line", (line) => {
|
|
|
204
227
|
} catch {
|
|
205
228
|
// ignore malformed input
|
|
206
229
|
}
|
|
207
|
-
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function startServer({
|
|
233
|
+
input = process.stdin,
|
|
234
|
+
output = process.stdout,
|
|
235
|
+
} = {}) {
|
|
236
|
+
const rl = createInterface({ input, terminal: false });
|
|
237
|
+
const handleMessage = createMessageHandler((line) => output.write(line));
|
|
238
|
+
rl.on("line", (line) => processLine(line, handleMessage));
|
|
239
|
+
return rl;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const isDirectExecution =
|
|
243
|
+
process.argv[1] !== undefined &&
|
|
244
|
+
import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
245
|
+
|
|
246
|
+
if (isDirectExecution) {
|
|
247
|
+
startServer();
|
|
248
|
+
}
|