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.
Files changed (131) hide show
  1. package/.claude-plugin/marketplace.json +4 -4
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +3 -3
  4. package/CHANGELOG.md +55 -0
  5. package/README.md +201 -18
  6. package/commands/post.md +14 -7
  7. package/commands/trending.md +4 -2
  8. package/context7.json +3 -1
  9. package/openclaw.plugin.json +2 -2
  10. package/package.json +9 -3
  11. package/scripts/check-reproducible.mjs +48 -0
  12. package/scripts/check-versions.mjs +12 -431
  13. package/scripts/release-guard/content-policy.mjs +77 -18
  14. package/scripts/release-guard/context.mjs +61 -0
  15. package/scripts/release-guard/frontmatter.mjs +3 -0
  16. package/scripts/release-guard/policy-checks.mjs +134 -0
  17. package/scripts/release-guard/policy.mjs +4 -1
  18. package/scripts/release-guard/repository-checks.mjs +256 -0
  19. package/server.json +2 -2
  20. package/skills/x-twitter-scraper/SKILL.md +106 -11
  21. package/skills/x-twitter-scraper/metadata.json +1 -1
  22. package/skills/x-twitter-scraper/references/api-endpoints-api-keys.md +6 -23
  23. package/skills/x-twitter-scraper/references/api-endpoints-drafts.md +20 -1
  24. package/skills/x-twitter-scraper/references/api-endpoints-draws.md +26 -0
  25. package/skills/x-twitter-scraper/references/api-endpoints-error-codes.md +3 -0
  26. package/skills/x-twitter-scraper/references/api-endpoints-events.md +3 -1
  27. package/skills/x-twitter-scraper/references/api-endpoints-extractions.md +41 -26
  28. package/skills/x-twitter-scraper/references/api-endpoints-monitors.md +43 -6
  29. package/skills/x-twitter-scraper/references/api-endpoints-radar.md +2 -1
  30. package/skills/x-twitter-scraper/references/api-endpoints-support.md +22 -0
  31. package/skills/x-twitter-scraper/references/api-endpoints-trends.md +9 -1
  32. package/skills/x-twitter-scraper/references/api-endpoints-tweet-style-cache.md +32 -1
  33. package/skills/x-twitter-scraper/references/api-endpoints-webhooks.md +55 -9
  34. package/skills/x-twitter-scraper/references/api-endpoints-x-accounts.md +12 -0
  35. package/skills/x-twitter-scraper/references/api-endpoints-x-api.md +51 -11
  36. package/skills/x-twitter-scraper/references/api-endpoints-x-media.md +10 -0
  37. package/skills/x-twitter-scraper/references/api-endpoints-x-write.md +85 -19
  38. package/skills/x-twitter-scraper/references/automate-twitter-giveaways.md +130 -0
  39. package/skills/x-twitter-scraper/references/best-x-api-alternative.md +198 -0
  40. package/skills/x-twitter-scraper/references/compare-twitter-apis.md +152 -0
  41. package/skills/x-twitter-scraper/references/draws.md +1 -1
  42. package/skills/x-twitter-scraper/references/export-twitter-followers.md +127 -0
  43. package/skills/x-twitter-scraper/references/extract-x-community-data.md +121 -0
  44. package/skills/x-twitter-scraper/references/extractions.md +74 -6
  45. package/skills/x-twitter-scraper/references/mcp-setup.md +14 -5
  46. package/skills/x-twitter-scraper/references/mcp-tools.md +14 -4
  47. package/skills/x-twitter-scraper/references/monitor-twitter-webhooks.md +122 -0
  48. package/skills/x-twitter-scraper/references/python-examples.md +39 -12
  49. package/skills/x-twitter-scraper/references/reliable-twitter-data-api-2026.md +241 -0
  50. package/skills/x-twitter-scraper/references/scrape-export-twitter-data.md +171 -0
  51. package/skills/x-twitter-scraper/references/track-twitter-keywords-mentions.md +148 -0
  52. package/skills/x-twitter-scraper/references/twitter-api-alternative-faq.md +128 -0
  53. package/skills/x-twitter-scraper/references/twitter-api-without-x-account.md +111 -0
  54. package/skills/x-twitter-scraper/references/twitter-data-pipeline.md +136 -0
  55. package/skills/x-twitter-scraper/references/twitter-scraper-api-guide.md +229 -0
  56. package/skills/x-twitter-scraper/references/types-events.md +5 -25
  57. package/skills/x-twitter-scraper/references/types-extractions.md +49 -2
  58. package/skills/x-twitter-scraper/references/types-mcp-output-schemas.md +10 -19
  59. package/skills/x-twitter-scraper/references/types-monitors.md +30 -1
  60. package/skills/x-twitter-scraper/references/types-radar.md +7 -0
  61. package/skills/x-twitter-scraper/references/types-rest-api-vs-mcp-field-naming.md +8 -12
  62. package/skills/x-twitter-scraper/references/types-trends.md +3 -0
  63. package/skills/x-twitter-scraper/references/types-webhooks.md +10 -2
  64. package/skills/x-twitter-scraper/references/types-x-api.md +93 -12
  65. package/skills/x-twitter-scraper/references/types-x-articles.md +27 -12
  66. package/skills/x-twitter-scraper/references/types-x-write.md +46 -7
  67. package/skills/x-twitter-scraper/references/types.md +0 -17
  68. package/skills/x-twitter-scraper/references/webhooks.md +123 -35
  69. package/skills/x-twitter-scraper/references/workflows.md +27 -9
  70. package/skills/x-twitter-scraper/skill-card.md +2 -2
  71. package/skills/x-twitter-scraper/skillspector-report.md +5 -2
  72. package/skills/xquik-social-research/SKILL.md +1 -0
  73. package/start.sh +3 -0
  74. package/stub-server.mjs +91 -50
  75. package/task-guides/check-mutuals.md +1 -1
  76. package/task-guides/clean-followers.md +1 -1
  77. package/task-guides/export-tweets-csv.md +14 -5
  78. package/task-guides/extract-followers.md +2 -2
  79. package/task-guides/find-bangers.md +4 -2
  80. package/task-guides/find-influencers.md +7 -4
  81. package/task-guides/find-viral-tweets.md +1 -1
  82. package/task-guides/follow-unfollow.md +9 -4
  83. package/task-guides/for-you-feed.md +1 -1
  84. package/task-guides/going-viral.md +1 -1
  85. package/task-guides/grow-followers.md +4 -2
  86. package/task-guides/monitor-accounts.md +8 -9
  87. package/task-guides/optimize-tweets.md +1 -1
  88. package/task-guides/post-tweets.md +11 -7
  89. package/task-guides/run-giveaway.md +1 -1
  90. package/task-guides/search-tweets.md +16 -8
  91. package/task-guides/send-dms.md +16 -7
  92. package/task-guides/top-replies.md +25 -9
  93. package/task-guides/track-competitors.md +3 -3
  94. package/task-guides/track-hashtags.md +7 -8
  95. package/task-guides/track-mentions.md +10 -9
  96. package/task-guides/trending-news.md +13 -8
  97. package/task-guides/tweet-analytics.md +2 -2
  98. package/task-guides/tweet-ideas.md +1 -1
  99. package/task-guides/tweet-replies.md +63 -14
  100. package/task-guides/tweet-style.md +1 -1
  101. package/task-guides/tweet-webhooks.md +16 -12
  102. package/task-guides/update-x-profile.md +8 -5
  103. package/task-guides/user-tweets.md +20 -8
  104. package/task-guides/who-liked.md +1 -1
  105. package/task-guides/who-quoted.md +1 -1
  106. package/task-guides/who-retweeted.md +1 -1
  107. package/task-guides/write-threads.md +1 -1
  108. package/task-guides/write-tweets.md +1 -1
  109. package/task-guides/x-articles.md +10 -5
  110. package/task-guides/x-bookmarks.md +1 -1
  111. package/task-guides/x-communities.md +1 -1
  112. package/task-guides/x-lists.md +1 -1
  113. package/task-guides/x-spaces.md +1 -1
  114. package/task-guides/x-trends.md +6 -6
  115. package/skills/x-twitter-scraper/references/types-mcp-add-webhook.md +0 -14
  116. package/skills/x-twitter-scraper/references/types-mcp-check-follow.md +0 -10
  117. package/skills/x-twitter-scraper/references/types-mcp-compose-tweet.md +0 -31
  118. package/skills/x-twitter-scraper/references/types-mcp-estimate-extraction.md +0 -15
  119. package/skills/x-twitter-scraper/references/types-mcp-get-account.md +0 -26
  120. package/skills/x-twitter-scraper/references/types-mcp-get-draw.md +0 -30
  121. package/skills/x-twitter-scraper/references/types-mcp-get-events.md +0 -19
  122. package/skills/x-twitter-scraper/references/types-mcp-get-trends.md +0 -16
  123. package/skills/x-twitter-scraper/references/types-mcp-get-user-info.md +0 -16
  124. package/skills/x-twitter-scraper/references/types-mcp-list-monitors.md +0 -15
  125. package/skills/x-twitter-scraper/references/types-mcp-lookup-tweet.md +0 -25
  126. package/skills/x-twitter-scraper/references/types-mcp-refine-tweet.md +0 -13
  127. package/skills/x-twitter-scraper/references/types-mcp-run-draw.md +0 -18
  128. package/skills/x-twitter-scraper/references/types-mcp-run-extraction.md +0 -12
  129. package/skills/x-twitter-scraper/references/types-mcp-score-tweet.md +0 -16
  130. package/skills/x-twitter-scraper/references/types-mcp-search-tweets.md +0 -17
  131. 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
- The `X-Xquik-Signature` header contains: `sha256=` + HMAC-SHA256(secret, raw JSON body).
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 verifySignature(payload, signature, secret) {
52
- if (typeof signature !== "string" || !secret) return false;
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
- const expected = "sha256=" + createHmac("sha256", secret).update(payload).digest("hex");
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 (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
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
- switch (event.eventType) {
85
- case "tweet.new":
86
- console.log(`New tweet from @${event.username}: ${event.data.text}`);
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
- def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
119
- expected = "sha256=" + hmac.new(
120
- secret.encode(), payload, hashlib.sha256
121
- ).hexdigest()
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["eventType"] == "tweet.new":
139
- print(f"New tweet from @{event['username']}: {event['data']['text']}")
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
- fmt.Printf("[%s] @%s: %s\n", event.EventType, event.Username, event.Data.Text)
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 the raw request body.** Compute HMAC over raw bytes, not re-serialized JSON
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 on failure, delivering the same event multiple times. Deduplicate by hashing the raw payload:
297
+ Webhook deliveries can retry. Deduplicate by `deliveryId` in durable storage:
214
298
 
215
299
  ```javascript
216
- import { createHash } from "node:crypto";
300
+ const processedDeliveries = new Set(); // Use durable storage in production
217
301
 
218
- const processedPayloads = new Set(); // Use Redis/DB in production
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
- processedPayloads.add(payloadHash);
305
+ processedDeliveries.add(event.deliveryId);
225
306
  }
226
307
  ```
227
308
 
228
309
  ## Retry Policy
229
310
 
230
- Failed deliveries are retried up to 5 times with exponential backoff. Delivery statuses: `pending`, `delivered`, `failed`, `exhausted`.
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 `4xx` (except 429). Max 3 retries:
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. When more results exist, the response includes `hasMore: true` and a `nextCursor` string. Pass `nextCursor` as the `after` query parameter.
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("after", cursor);
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 ? `?after=${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 as CSV/JSON/MD/MD-document/PDF/TXT/XLSX (100,000 row limit; PDF 10,000)
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
- Event types: `tweet.new`, `tweet.quote`, `tweet.reply`, `tweet.retweet`, `webhook.test`.
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` | Metered per result |
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-16 with 0 findings. Refresh it after each skill directory change.
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.5.6
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-16 13:37:35 UTC
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: 75
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
@@ -1,2 +1,5 @@
1
1
  #!/bin/sh
2
+ # SPDX-FileCopyrightText: 2026 Xquik Contributors
3
+ # SPDX-License-Identifier: MIT
4
+
2
5
  exec node stub-server.mjs
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.5.6",
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 119-route API catalog before calling 'xquik'. This package stub returns setup guidance only.",
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 119 routes. Of these, 118 support JSON or text.",
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 119 catalog routes. This package stub returns setup guidance only.",
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
- "- 118 catalog routes support JSON or text. Binary support downloads use REST.",
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 rl = createInterface({ input: process.stdin, terminal: false });
131
-
132
- function send(msg) {
133
- const json = JSON.stringify(msg);
134
- process.stdout.write(json + "\n");
135
- }
136
-
137
- function sendResult(id, result) {
138
- send({ jsonrpc: JSONRPC, id, result });
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
- function sendError(id, code, message) {
142
- send({ jsonrpc: JSONRPC, id, error: { code, message } });
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 sendStubToolResult(id) {
154
- sendResult(id, {
155
- content: [{ type: "text", text: LIVE_SERVER_MESSAGE }],
156
- });
157
- }
161
+ export function createMessageHandler(writeLine) {
162
+ function send(msg) {
163
+ writeLine(`${JSON.stringify(msg)}\n`);
164
+ }
158
165
 
159
- function handleMessage(msg) {
160
- const { id, method, params } = msg;
166
+ function sendResult(id, result) {
167
+ send({ jsonrpc: JSONRPC, id, result });
168
+ }
161
169
 
162
- switch (method) {
163
- case "initialize":
164
- return sendResult(id, {
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
- case "notifications/initialized":
171
- return; // no response needed
174
+ function sendStubToolResult(id) {
175
+ sendResult(id, {
176
+ content: [{ type: "text", text: LIVE_SERVER_MESSAGE }],
177
+ });
178
+ }
172
179
 
173
- case "tools/list":
174
- return sendResult(id, { tools: TOOLS });
180
+ return function handleMessage(msg) {
181
+ const { id, method, params } = msg;
182
+ if (id === undefined) {
183
+ return;
184
+ }
175
185
 
176
- case "tools/call": {
177
- const toolName = params?.name;
178
- if (isKnownTool(toolName)) {
179
- return sendStubToolResult(id);
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
- case "ping":
185
- return sendResult(id, {});
208
+ case "ping":
209
+ return sendResult(id, {});
186
210
 
187
- default:
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
- rl.on("line", (line) => {
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
+ }
@@ -5,7 +5,7 @@ license: MIT
5
5
  metadata:
6
6
  internal: true
7
7
  author: Xquik
8
- version: "2.5.6"
8
+ version: "2.6.2"
9
9
  openclaw:
10
10
  requires:
11
11
  env: