xtrawl 0.1.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.
Files changed (249) hide show
  1. package/DOCUMENTATION.md +774 -0
  2. package/README.md +232 -0
  3. package/dist/auth/bootstrap.d.ts +4 -0
  4. package/dist/auth/bootstrap.d.ts.map +1 -0
  5. package/dist/auth/bootstrap.js +29 -0
  6. package/dist/auth/bootstrap.js.map +1 -0
  7. package/dist/auth/cookies.d.ts +7 -0
  8. package/dist/auth/cookies.d.ts.map +1 -0
  9. package/dist/auth/cookies.js +83 -0
  10. package/dist/auth/cookies.js.map +1 -0
  11. package/dist/auth/loaders.d.ts +6 -0
  12. package/dist/auth/loaders.d.ts.map +1 -0
  13. package/dist/auth/loaders.js +69 -0
  14. package/dist/auth/loaders.js.map +1 -0
  15. package/dist/auth/material.d.ts +3 -0
  16. package/dist/auth/material.d.ts.map +1 -0
  17. package/dist/auth/material.js +20 -0
  18. package/dist/auth/material.js.map +1 -0
  19. package/dist/auth/records.d.ts +4 -0
  20. package/dist/auth/records.d.ts.map +1 -0
  21. package/dist/auth/records.js +38 -0
  22. package/dist/auth/records.js.map +1 -0
  23. package/dist/cli/help.d.ts +2 -0
  24. package/dist/cli/help.d.ts.map +1 -0
  25. package/dist/cli/help.js +30 -0
  26. package/dist/cli/help.js.map +1 -0
  27. package/dist/cli/main.d.ts +3 -0
  28. package/dist/cli/main.d.ts.map +1 -0
  29. package/dist/cli/main.js +85 -0
  30. package/dist/cli/main.js.map +1 -0
  31. package/dist/cli/parser.d.ts +22 -0
  32. package/dist/cli/parser.d.ts.map +1 -0
  33. package/dist/cli/parser.js +275 -0
  34. package/dist/cli/parser.js.map +1 -0
  35. package/dist/client/client.d.ts +29 -0
  36. package/dist/client/client.d.ts.map +1 -0
  37. package/dist/client/client.js +192 -0
  38. package/dist/client/client.js.map +1 -0
  39. package/dist/client/collectors.d.ts +15 -0
  40. package/dist/client/collectors.d.ts.map +1 -0
  41. package/dist/client/collectors.js +185 -0
  42. package/dist/client/collectors.js.map +1 -0
  43. package/dist/client/database.d.ts +56 -0
  44. package/dist/client/database.d.ts.map +1 -0
  45. package/dist/client/database.js +146 -0
  46. package/dist/client/database.js.map +1 -0
  47. package/dist/client/profiles.d.ts +5 -0
  48. package/dist/client/profiles.d.ts.map +1 -0
  49. package/dist/client/profiles.js +36 -0
  50. package/dist/client/profiles.js.map +1 -0
  51. package/dist/client/search.d.ts +21 -0
  52. package/dist/client/search.d.ts.map +1 -0
  53. package/dist/client/search.js +139 -0
  54. package/dist/client/search.js.map +1 -0
  55. package/dist/client/types.d.ts +21 -0
  56. package/dist/client/types.d.ts.map +1 -0
  57. package/dist/client/types.js +2 -0
  58. package/dist/client/types.js.map +1 -0
  59. package/dist/config/defaults.d.ts +3 -0
  60. package/dist/config/defaults.d.ts.map +1 -0
  61. package/dist/config/defaults.js +38 -0
  62. package/dist/config/defaults.js.map +1 -0
  63. package/dist/config/environment.d.ts +5 -0
  64. package/dist/config/environment.d.ts.map +1 -0
  65. package/dist/config/environment.js +62 -0
  66. package/dist/config/environment.js.map +1 -0
  67. package/dist/config/types.d.ts +47 -0
  68. package/dist/config/types.d.ts.map +1 -0
  69. package/dist/config/types.js +2 -0
  70. package/dist/config/types.js.map +1 -0
  71. package/dist/config/validation.d.ts +6 -0
  72. package/dist/config/validation.d.ts.map +1 -0
  73. package/dist/config/validation.js +147 -0
  74. package/dist/config/validation.js.map +1 -0
  75. package/dist/domain/accounts.d.ts +64 -0
  76. package/dist/domain/accounts.d.ts.map +1 -0
  77. package/dist/domain/accounts.js +2 -0
  78. package/dist/domain/accounts.js.map +1 -0
  79. package/dist/domain/errors.d.ts +61 -0
  80. package/dist/domain/errors.d.ts.map +1 -0
  81. package/dist/domain/errors.js +89 -0
  82. package/dist/domain/errors.js.map +1 -0
  83. package/dist/domain/http.d.ts +36 -0
  84. package/dist/domain/http.d.ts.map +1 -0
  85. package/dist/domain/http.js +2 -0
  86. package/dist/domain/http.js.map +1 -0
  87. package/dist/domain/manifest.d.ts +23 -0
  88. package/dist/domain/manifest.d.ts.map +1 -0
  89. package/dist/domain/manifest.js +2 -0
  90. package/dist/domain/manifest.js.map +1 -0
  91. package/dist/domain/records.d.ts +67 -0
  92. package/dist/domain/records.d.ts.map +1 -0
  93. package/dist/domain/records.js +2 -0
  94. package/dist/domain/records.js.map +1 -0
  95. package/dist/domain/requests.d.ts +87 -0
  96. package/dist/domain/requests.d.ts.map +1 -0
  97. package/dist/domain/requests.js +2 -0
  98. package/dist/domain/requests.js.map +1 -0
  99. package/dist/engine/api-engine.d.ts +25 -0
  100. package/dist/engine/api-engine.d.ts.map +1 -0
  101. package/dist/engine/api-engine.js +98 -0
  102. package/dist/engine/api-engine.js.map +1 -0
  103. package/dist/engine/extractors.d.ts +27 -0
  104. package/dist/engine/extractors.d.ts.map +1 -0
  105. package/dist/engine/extractors.js +248 -0
  106. package/dist/engine/extractors.js.map +1 -0
  107. package/dist/index.d.ts +21 -0
  108. package/dist/index.d.ts.map +1 -0
  109. package/dist/index.js +15 -0
  110. package/dist/index.js.map +1 -0
  111. package/dist/manifest/default-manifest.d.ts +3 -0
  112. package/dist/manifest/default-manifest.d.ts.map +1 -0
  113. package/dist/manifest/default-manifest.js +46 -0
  114. package/dist/manifest/default-manifest.js.map +1 -0
  115. package/dist/manifest/model.d.ts +3 -0
  116. package/dist/manifest/model.d.ts.map +1 -0
  117. package/dist/manifest/model.js +48 -0
  118. package/dist/manifest/model.js.map +1 -0
  119. package/dist/manifest/provider.d.ts +19 -0
  120. package/dist/manifest/provider.d.ts.map +1 -0
  121. package/dist/manifest/provider.js +94 -0
  122. package/dist/manifest/provider.js.map +1 -0
  123. package/dist/manifest/scraper.d.ts +11 -0
  124. package/dist/manifest/scraper.d.ts.map +1 -0
  125. package/dist/manifest/scraper.js +130 -0
  126. package/dist/manifest/scraper.js.map +1 -0
  127. package/dist/output/csv-writer.d.ts +2 -0
  128. package/dist/output/csv-writer.d.ts.map +1 -0
  129. package/dist/output/csv-writer.js +94 -0
  130. package/dist/output/csv-writer.js.map +1 -0
  131. package/dist/output/json-writer.d.ts +2 -0
  132. package/dist/output/json-writer.d.ts.map +1 -0
  133. package/dist/output/json-writer.js +29 -0
  134. package/dist/output/json-writer.js.map +1 -0
  135. package/dist/output/names.d.ts +4 -0
  136. package/dist/output/names.d.ts.map +1 -0
  137. package/dist/output/names.js +22 -0
  138. package/dist/output/names.js.map +1 -0
  139. package/dist/output/writer.d.ts +7 -0
  140. package/dist/output/writer.d.ts.map +1 -0
  141. package/dist/output/writer.js +33 -0
  142. package/dist/output/writer.js.map +1 -0
  143. package/dist/pool/account-pool.d.ts +29 -0
  144. package/dist/pool/account-pool.d.ts.map +1 -0
  145. package/dist/pool/account-pool.js +145 -0
  146. package/dist/pool/account-pool.js.map +1 -0
  147. package/dist/pool/cooldown.d.ts +18 -0
  148. package/dist/pool/cooldown.d.ts.map +1 -0
  149. package/dist/pool/cooldown.js +38 -0
  150. package/dist/pool/cooldown.js.map +1 -0
  151. package/dist/pool/limiter.d.ts +11 -0
  152. package/dist/pool/limiter.d.ts.map +1 -0
  153. package/dist/pool/limiter.js +27 -0
  154. package/dist/pool/limiter.js.map +1 -0
  155. package/dist/query/builder.d.ts +19 -0
  156. package/dist/query/builder.d.ts.map +1 -0
  157. package/dist/query/builder.js +110 -0
  158. package/dist/query/builder.js.map +1 -0
  159. package/dist/query/hash.d.ts +2 -0
  160. package/dist/query/hash.d.ts.map +1 -0
  161. package/dist/query/hash.js +6 -0
  162. package/dist/query/hash.js.map +1 -0
  163. package/dist/query/normalize.d.ts +39 -0
  164. package/dist/query/normalize.d.ts.map +1 -0
  165. package/dist/query/normalize.js +176 -0
  166. package/dist/query/normalize.js.map +1 -0
  167. package/dist/query/targets.d.ts +11 -0
  168. package/dist/query/targets.d.ts.map +1 -0
  169. package/dist/query/targets.js +96 -0
  170. package/dist/query/targets.js.map +1 -0
  171. package/dist/runner/index.d.ts +4 -0
  172. package/dist/runner/index.d.ts.map +1 -0
  173. package/dist/runner/index.js +4 -0
  174. package/dist/runner/index.js.map +1 -0
  175. package/dist/runner/retry.d.ts +7 -0
  176. package/dist/runner/retry.d.ts.map +1 -0
  177. package/dist/runner/retry.js +26 -0
  178. package/dist/runner/retry.js.map +1 -0
  179. package/dist/runner/runner.d.ts +20 -0
  180. package/dist/runner/runner.d.ts.map +1 -0
  181. package/dist/runner/runner.js +37 -0
  182. package/dist/runner/runner.js.map +1 -0
  183. package/dist/runner/task-queue.d.ts +19 -0
  184. package/dist/runner/task-queue.d.ts.map +1 -0
  185. package/dist/runner/task-queue.js +58 -0
  186. package/dist/runner/task-queue.js.map +1 -0
  187. package/dist/storage/account-repository.d.ts +50 -0
  188. package/dist/storage/account-repository.d.ts.map +1 -0
  189. package/dist/storage/account-repository.js +171 -0
  190. package/dist/storage/account-repository.js.map +1 -0
  191. package/dist/storage/account-row.d.ts +4 -0
  192. package/dist/storage/account-row.d.ts.map +1 -0
  193. package/dist/storage/account-row.js +46 -0
  194. package/dist/storage/account-row.js.map +1 -0
  195. package/dist/storage/checkpoint-repository.d.ts +10 -0
  196. package/dist/storage/checkpoint-repository.d.ts.map +1 -0
  197. package/dist/storage/checkpoint-repository.js +30 -0
  198. package/dist/storage/checkpoint-repository.js.map +1 -0
  199. package/dist/storage/database.d.ts +18 -0
  200. package/dist/storage/database.d.ts.map +1 -0
  201. package/dist/storage/database.js +42 -0
  202. package/dist/storage/database.js.map +1 -0
  203. package/dist/storage/index.d.ts +15 -0
  204. package/dist/storage/index.d.ts.map +1 -0
  205. package/dist/storage/index.js +18 -0
  206. package/dist/storage/index.js.map +1 -0
  207. package/dist/storage/manifest-repository.d.ts +9 -0
  208. package/dist/storage/manifest-repository.d.ts.map +1 -0
  209. package/dist/storage/manifest-repository.js +22 -0
  210. package/dist/storage/manifest-repository.js.map +1 -0
  211. package/dist/storage/run-repository.d.ts +24 -0
  212. package/dist/storage/run-repository.d.ts.map +1 -0
  213. package/dist/storage/run-repository.js +51 -0
  214. package/dist/storage/run-repository.js.map +1 -0
  215. package/dist/storage/schema.d.ts +3 -0
  216. package/dist/storage/schema.d.ts.map +1 -0
  217. package/dist/storage/schema.js +53 -0
  218. package/dist/storage/schema.js.map +1 -0
  219. package/dist/transport/graphql.d.ts +8 -0
  220. package/dist/transport/graphql.d.ts.map +1 -0
  221. package/dist/transport/graphql.js +104 -0
  222. package/dist/transport/graphql.js.map +1 -0
  223. package/dist/transport/proxy.d.ts +5 -0
  224. package/dist/transport/proxy.d.ts.map +1 -0
  225. package/dist/transport/proxy.js +36 -0
  226. package/dist/transport/proxy.js.map +1 -0
  227. package/dist/transport/session.d.ts +25 -0
  228. package/dist/transport/session.d.ts.map +1 -0
  229. package/dist/transport/session.js +138 -0
  230. package/dist/transport/session.js.map +1 -0
  231. package/dist/transport/transaction-id.d.ts +20 -0
  232. package/dist/transport/transaction-id.d.ts.map +1 -0
  233. package/dist/transport/transaction-id.js +74 -0
  234. package/dist/transport/transaction-id.js.map +1 -0
  235. package/dist/utils/fingerprint.d.ts +3 -0
  236. package/dist/utils/fingerprint.d.ts.map +1 -0
  237. package/dist/utils/fingerprint.js +20 -0
  238. package/dist/utils/fingerprint.js.map +1 -0
  239. package/dist/utils/guards.d.ts +8 -0
  240. package/dist/utils/guards.d.ts.map +1 -0
  241. package/dist/utils/guards.js +38 -0
  242. package/dist/utils/guards.js.map +1 -0
  243. package/docs/architecture/overview.md +59 -0
  244. package/docs/decisions/0001-runtime-and-state.md +21 -0
  245. package/docs/decisions/0002-modular-boundaries.md +16 -0
  246. package/docs/plans/initial-build.md +18 -0
  247. package/docs/product/specification.md +87 -0
  248. package/docs/security/data-boundary.md +27 -0
  249. package/package.json +89 -0
@@ -0,0 +1,774 @@
1
+ # XTrawl documentation
2
+
3
+ This guide explains how to install, authenticate, configure, and operate XTrawl from TypeScript or
4
+ the command line. XTrawl is an authenticated, read-only collector for public X data.
5
+
6
+ ## Contents
7
+
8
+ - [Install](#install)
9
+ - [Authenticate](#authenticate)
10
+ - [Use multiple accounts](#use-multiple-accounts)
11
+ - [Use a proxy](#use-a-proxy)
12
+ - [Use the TypeScript API](#use-the-typescript-api)
13
+ - [Search filters](#search-filters)
14
+ - [Use the CLI](#use-the-cli)
15
+ - [Control pagination and resume runs](#control-pagination-and-resume-runs)
16
+ - [Save output](#save-output)
17
+ - [Configure XTrawl](#configure-xtrawl)
18
+ - [Understand return types](#understand-return-types)
19
+ - [Handle errors](#handle-errors)
20
+ - [Refresh operation identifiers](#refresh-operation-identifiers)
21
+ - [Understand storage and account health](#understand-storage-and-account-health)
22
+ - [Manage local state](#manage-local-state)
23
+ - [Protect credentials and collected data](#protect-credentials-and-collected-data)
24
+ - [Troubleshoot common problems](#troubleshoot-common-problems)
25
+ - [Know the limitations](#know-the-limitations)
26
+
27
+ ## Install
28
+
29
+ Requirements:
30
+
31
+ - Node.js 22.5 or newer
32
+ - npm
33
+ - An X browser session that you own or are authorized to use
34
+
35
+ Install the package:
36
+
37
+ ```bash
38
+ npm install xtrawl
39
+ ```
40
+
41
+ To contribute or run the source checkout:
42
+
43
+ ```bash
44
+ git clone https://github.com/ensp1re/xtrawl.git
45
+ cd xtrawl
46
+ npm install
47
+ npm run build
48
+ ```
49
+
50
+ The source build writes ESM output to `dist/`. In source-checkout commands, replace `xtrawl` with
51
+ `npm run cli --`.
52
+
53
+ Verify the checkout:
54
+
55
+ ```bash
56
+ npm run check
57
+ ```
58
+
59
+ ## Authenticate
60
+
61
+ Live requests require the `auth_token` and `ct0` cookies from an authenticated X browser session.
62
+ XTrawl calls these values `authToken` and `csrfToken` in TypeScript and reads them from
63
+ `X_AUTH_TOKEN` and `X_CSRF_TOKEN` in the process environment.
64
+
65
+ ### Use environment variables
66
+
67
+ ```bash
68
+ export X_AUTH_TOKEN="your-auth-token"
69
+ export X_CSRF_TOKEN="your-ct0-token"
70
+
71
+ xtrawl user-info OpenAI --pretty
72
+ ```
73
+
74
+ This is the smallest setup for a single account.
75
+
76
+ ### Use an environment file
77
+
78
+ Create a local file that is not committed, for example `.env.local`:
79
+
80
+ ```dotenv
81
+ X_AUTH_TOKEN=your-auth-token
82
+ X_CSRF_TOKEN=your-ct0-token
83
+ ```
84
+
85
+ Pass global options before the command:
86
+
87
+ ```bash
88
+ xtrawl --env-file .env.local user-info OpenAI --pretty
89
+ ```
90
+
91
+ Environment files also recognize `AUTH_TOKEN` for the auth cookie and `CT0` or `CSRF` for the CSRF
92
+ cookie. Prefer the `X_AUTH_TOKEN` and `X_CSRF_TOKEN` names for clarity.
93
+
94
+ ### Bootstrap a missing CSRF cookie
95
+
96
+ `XTrawl.create()` checks provisioned accounts that have an auth token but no CSRF token. It attempts
97
+ to obtain the missing `ct0` cookie before returning the client:
98
+
99
+ ```ts
100
+ const client = await XTrawl.create({
101
+ authToken: process.env.X_AUTH_TOKEN,
102
+ });
103
+ ```
104
+
105
+ Supplying both cookies is more deterministic. A session without usable authentication is not
106
+ eligible for live requests.
107
+
108
+ ## Use multiple accounts
109
+
110
+ XTrawl can load account records from inline objects or a file and keep account health in SQLite.
111
+ For JSON files, use one object or an array of objects:
112
+
113
+ ```json
114
+ [
115
+ {
116
+ "username": "collector-one",
117
+ "authToken": "replace-me",
118
+ "csrfToken": "replace-me"
119
+ },
120
+ {
121
+ "username": "collector-two",
122
+ "cookies": {
123
+ "auth_token": "replace-me",
124
+ "ct0": "replace-me"
125
+ },
126
+ "proxy": "http://127.0.0.1:8080"
127
+ }
128
+ ]
129
+ ```
130
+
131
+ Never commit this file. Load it from the CLI:
132
+
133
+ ```bash
134
+ xtrawl --cookies-file ./accounts.json search "typescript" --limit 100
135
+ ```
136
+
137
+ Or from TypeScript:
138
+
139
+ ```ts
140
+ const client = await XTrawl.create({
141
+ accountsFile: "./accounts.json",
142
+ dbPath: "./state/xtrawl.db",
143
+ });
144
+ ```
145
+
146
+ Supported account sources are:
147
+
148
+ | Source | CLI or library option | Notes |
149
+ | --- | --- | --- |
150
+ | Process environment | No option required | Reads `X_AUTH_TOKEN` and `X_CSRF_TOKEN` |
151
+ | Environment file | `--env-file` / `envFile` | Reads dotenv-style values |
152
+ | JSON account file | `--cookies-file` / `cookiesFile` or `accountsFile` | Accepts one account or an array |
153
+ | Netscape cookie file | `--cookies-file` / `cookiesFile` | Extracts browser cookie records |
154
+ | Delimited account file | `--cookies-file` / `cookiesFile` | Accepts pipe- or colon-separated records |
155
+ | Inline account records | `accounts` | TypeScript API only |
156
+ | Inline cookie payload | `cookies` | TypeScript API only; accepts an object or cookie string |
157
+
158
+ Delimited files are accepted for account import compatibility, but live sessions still require
159
+ usable `auth_token` and `ct0` values. XTrawl does not perform interactive username/password login.
160
+
161
+ If you want to reuse accounts already stored in the configured SQLite database without provisioning
162
+ new input, create the client with `provision: false`.
163
+
164
+ ## Use a proxy
165
+
166
+ Set one proxy for all accounts:
167
+
168
+ ```bash
169
+ xtrawl --proxy http://127.0.0.1:8080 search "typescript" --limit 20
170
+ ```
171
+
172
+ The TypeScript API accepts a URL or structured proxy settings:
173
+
174
+ ```ts
175
+ const client = await XTrawl.create({
176
+ authToken: process.env.X_AUTH_TOKEN,
177
+ csrfToken: process.env.X_CSRF_TOKEN,
178
+ proxy: {
179
+ scheme: "http",
180
+ host: "127.0.0.1",
181
+ port: 8080,
182
+ },
183
+ });
184
+ ```
185
+
186
+ An account record may define its own `proxy`; that account-level value takes precedence for its
187
+ session. HTTP, HTTPS, and SOCKS5 URLs are supported. Before leasing a proxied account, XTrawl performs
188
+ a short unauthenticated health request through that proxy and caches a successful result for one
189
+ minute. Keep proxy credentials in secret storage, not in committed configuration.
190
+
191
+ ## Use the TypeScript API
192
+
193
+ The package is ESM:
194
+
195
+ ```ts
196
+ import {
197
+ AuthError,
198
+ RateLimitError,
199
+ XTrawl,
200
+ type SearchResult,
201
+ } from "xtrawl";
202
+ ```
203
+
204
+ ### Create and close a client
205
+
206
+ ```ts
207
+ const client = await XTrawl.create({
208
+ authToken: process.env.X_AUTH_TOKEN,
209
+ csrfToken: process.env.X_CSRF_TOKEN,
210
+ dbPath: "xtrawl.db",
211
+ saveDir: "outputs",
212
+ });
213
+
214
+ try {
215
+ // Run operations here.
216
+ } finally {
217
+ client.close();
218
+ }
219
+ ```
220
+
221
+ Always close the client when the process no longer needs it so the SQLite connection is released.
222
+
223
+ The synchronous constructor is also public:
224
+
225
+ ```ts
226
+ const client = new XTrawl({
227
+ authToken: process.env.X_AUTH_TOKEN,
228
+ csrfToken: process.env.X_CSRF_TOKEN,
229
+ });
230
+ ```
231
+
232
+ Prefer `XTrawl.create()` because it can bootstrap a missing CSRF cookie.
233
+
234
+ ### Search posts
235
+
236
+ ```ts
237
+ const result: SearchResult = await client.search("typescript", {
238
+ since: "2026-01-01",
239
+ until: "2026-02-01",
240
+ fromUsers: ["OpenAI", "github"],
241
+ exactPhrases: ["open source"],
242
+ excludeWords: ["hiring"],
243
+ lang: "en",
244
+ minLikes: 10,
245
+ hasLinks: true,
246
+ tweetType: "originals_only",
247
+ displayType: "Latest",
248
+ limit: 200,
249
+ resume: true,
250
+ });
251
+
252
+ console.log(result.tweets);
253
+ console.log(result.stats);
254
+ ```
255
+
256
+ The first argument is the free-form search query. Typed options are normalized into X search
257
+ operators and combined with that query. If neither date bound is supplied, XTrawl searches the
258
+ previous 30 days. A bounded interval is split into up to `searchSplits` tasks and processed with the
259
+ available account concurrency.
260
+
261
+ ### Read profile information
262
+
263
+ ```ts
264
+ const profiles = await client.getUserInfo([
265
+ "OpenAI",
266
+ "@github",
267
+ { profileUrl: "https://x.com/typescript" },
268
+ ]);
269
+ ```
270
+
271
+ Each resolvable public target returns a normalized `ProfileRecord`.
272
+
273
+ Profile information can also be saved:
274
+
275
+ ```ts
276
+ await client.getUserInfo(["OpenAI", "github"], {
277
+ save: true,
278
+ saveFormat: "both",
279
+ });
280
+ ```
281
+
282
+ ### Read one post
283
+
284
+ Pass a numeric post ID or an X status URL:
285
+
286
+ ```ts
287
+ const tweet = await client.getTweet("https://x.com/OpenAI/status/1234567890");
288
+ ```
289
+
290
+ The method returns a normalized `TweetRecord`, or `undefined` when the response has no post result.
291
+
292
+ ### Collect profile posts
293
+
294
+ ```ts
295
+ const result = await client.getProfileTweets(["OpenAI", "github"], {
296
+ limit: 500,
297
+ perProfileLimit: 200,
298
+ maxPagesPerProfile: 20,
299
+ maxEmptyPages: 2,
300
+ resume: true,
301
+ save: true,
302
+ saveFormat: "both",
303
+ saveName: "profile-posts",
304
+ });
305
+ ```
306
+
307
+ `limit` caps all returned posts. `perProfileLimit` caps each target independently.
308
+
309
+ ### Collect followers and following
310
+
311
+ ```ts
312
+ const followers = await client.getFollowers(["OpenAI"], {
313
+ limit: 500,
314
+ resume: true,
315
+ });
316
+
317
+ const following = await client.getFollowing(["OpenAI"], {
318
+ perProfileLimit: 250,
319
+ });
320
+
321
+ const verifiedFollowers = await client.getVerifiedFollowers(["OpenAI"], {
322
+ maxPagesPerProfile: 10,
323
+ });
324
+ ```
325
+
326
+ These methods return normalized `FollowRecord` arrays. Records are deduplicated within each target.
327
+
328
+ ### Inspect local state
329
+
330
+ ```ts
331
+ const inspection = client.inspect();
332
+
333
+ console.log(inspection.config);
334
+ console.log(inspection.accounts);
335
+ ```
336
+
337
+ `inspect()` reports the validated configuration and redacted account projections. Tokens, cookie
338
+ values, passwords, email addresses, bearer overrides, and proxy credentials are not returned.
339
+
340
+ ## Search filters
341
+
342
+ `SearchRequest` supports these canonical options:
343
+
344
+ | Option | Type | Effect |
345
+ | --- | --- | --- |
346
+ | `searchQuery` | `string` | Free-form query; the first `search()` argument overrides it when non-empty |
347
+ | `since`, `until` | `string` | Lower and upper date bounds |
348
+ | `allWords` | `string[]` | Require every supplied term |
349
+ | `anyWords` | `string[]` | Require at least one supplied term |
350
+ | `exactPhrases` | `string[]` | Match quoted phrases |
351
+ | `excludeWords` | `string[]` | Exclude supplied terms |
352
+ | `hashtagsAny` | `string[]` | Match any supplied hashtag |
353
+ | `hashtagsExclude` | `string[]` | Exclude supplied hashtags |
354
+ | `fromUsers` | `string[]` | Match posts from these accounts |
355
+ | `toUsers` | `string[]` | Match posts addressed to these accounts |
356
+ | `mentioningUsers` | `string[]` | Match posts mentioning these accounts |
357
+ | `tweetType` | `TweetType` | Select or exclude originals, replies, or reposts |
358
+ | `verifiedOnly` | `boolean` | Require verified authors |
359
+ | `blueVerifiedOnly` | `boolean` | Require blue-verified authors |
360
+ | `hasImages` | `boolean` | Require images |
361
+ | `hasVideos` | `boolean` | Require videos |
362
+ | `hasLinks` | `boolean` | Require links |
363
+ | `hasMentions` | `boolean` | Require mentions |
364
+ | `hasHashtags` | `boolean` | Require hashtags |
365
+ | `minLikes` | `number` | Minimum like count |
366
+ | `minReplies` | `number` | Minimum reply count |
367
+ | `minRetweets` | `number` | Minimum repost count |
368
+ | `place` | `string` | Match an X place operator |
369
+ | `geocode` | `string` | Match a geocode expression |
370
+ | `near`, `within` | `string` | Match a named location and radius |
371
+ | `lang` | `string` | Match a language code |
372
+ | `displayType` | `"Top" \| "Latest"` | Select the search timeline mode |
373
+ | `limit` | `number` | Cap the number of returned posts |
374
+ | `maxEmptyPages` | `number` | Stop after this many consecutive empty pages |
375
+
376
+ `TweetType` accepts `all`, `originals_only`, `replies_only`, `retweets_only`, `exclude_replies`, or
377
+ `exclude_retweets`.
378
+
379
+ ## Use the CLI
380
+
381
+ Syntax:
382
+
383
+ ```text
384
+ xtrawl [global options] COMMAND [values] [command options]
385
+ ```
386
+
387
+ Use `npx xtrawl` if you installed XTrawl locally rather than globally. From a source checkout,
388
+ replace `xtrawl` with `npm run cli --`.
389
+
390
+ ### Commands
391
+
392
+ | Command | Values | Result |
393
+ | --- | --- | --- |
394
+ | `search` | Optional query | Matching public posts and run statistics |
395
+ | `tweet` | One or more post IDs or status URLs | Individual public post records |
396
+ | `profile-tweets` | One or more users | Posts from public profile timelines |
397
+ | `followers` | One or more users | Public follower relationships |
398
+ | `following` | One or more users | Public following relationships |
399
+ | `verified-followers` | One or more users | Public verified-follower relationships |
400
+ | `user-info` | One or more users | Public profile records |
401
+
402
+ ### Global options
403
+
404
+ Put these before the command:
405
+
406
+ | Option | Value | Purpose |
407
+ | --- | --- | --- |
408
+ | `--auth-token` | token | Supply one account's `auth_token` value |
409
+ | `--csrf-token`, `--ct0` | token | Supply one account's `ct0` value |
410
+ | `--cookies-file` | path | Load accounts or cookies from a file |
411
+ | `--env-file` | path | Load account values from a dotenv file |
412
+ | `--db-path` | path | Choose the SQLite state file |
413
+ | `--proxy` | URL | Set the default HTTP(S) or SOCKS5 proxy |
414
+ | `--concurrency` | positive integer | Set the configured worker count |
415
+ | `--manifest-scrape-on-init` | flag | Enable live operation-identifier refresh with local fallback |
416
+ | `--verbose`, `-v` | flag | Print redacted pool diagnostics and full error stacks |
417
+ | `--help` | flag | Print CLI help |
418
+
419
+ Example:
420
+
421
+ ```bash
422
+ xtrawl \
423
+ --env-file .env.local \
424
+ --db-path ./state/xtrawl.db \
425
+ search "typescript" \
426
+ --limit 100
427
+ ```
428
+
429
+ ### Search options
430
+
431
+ | CLI option | API option |
432
+ | --- | --- |
433
+ | `--since`, `--until` | `since`, `until` |
434
+ | `--from`, `--to`, `--mention` | `fromUsers`, `toUsers`, `mentioningUsers` |
435
+ | `--all-words`, `--any-words`, `--exact-phrases` | `allWords`, `anyWords`, `exactPhrases` |
436
+ | `--exclude-words` | `excludeWords` |
437
+ | `--hashtags-any`, `--hashtags-exclude` | `hashtagsAny`, `hashtagsExclude` |
438
+ | `--lang`, `--place`, `--geocode`, `--near`, `--within` | Location and language filters |
439
+ | `--display-type` | `displayType` |
440
+ | `--tweet-type` | `tweetType` |
441
+ | `--verified-only`, `--blue-verified-only` | Verification filters |
442
+ | `--has-images`, `--has-videos`, `--has-links` | Media and link filters |
443
+ | `--has-mentions`, `--has-hashtags` | Entity filters |
444
+ | `--min-likes`, `--min-replies`, `--min-retweets` | Engagement thresholds |
445
+ | `--limit`, `--max-empty-pages` | Pagination stop conditions |
446
+
447
+ Repeat list options or place multiple values after one list option:
448
+
449
+ ```bash
450
+ xtrawl search "release" \
451
+ --from OpenAI \
452
+ --from github \
453
+ --exact-phrases "open source" \
454
+ --lang en \
455
+ --limit 100
456
+ ```
457
+
458
+ ### Collection options
459
+
460
+ `profile-tweets`, `followers`, `following`, and `verified-followers` accept:
461
+
462
+ | Option | Purpose |
463
+ | --- | --- |
464
+ | `--limit` | Cap all returned records |
465
+ | `--per-profile-limit` | Cap records for each target |
466
+ | `--max-pages-per-profile` | Cap pages for each target |
467
+ | `--max-empty-pages` | Stop a target after consecutive empty pages |
468
+ | `--resume` | Read and update SQLite checkpoints |
469
+ | `--save` | Write output files |
470
+ | `--save-format csv\|json\|both` | Select output formats |
471
+ | `--save-dir` | Select the output directory |
472
+ | `--save-name` | Select the output filename without an extension |
473
+ | `--raw-json` | Include raw user payloads in relationship JSON output |
474
+
475
+ All commands accept `--pretty` after the command to print indented JSON to stdout. Without
476
+ `--pretty`, use `--save` when you need file output.
477
+
478
+ ## Control pagination and resume runs
479
+
480
+ XTrawl follows each response cursor until one of these conditions is met:
481
+
482
+ - The requested global or per-profile record limit is reached.
483
+ - The target reaches `maxPagesPerProfile`.
484
+ - The response has no next cursor.
485
+ - Consecutive empty pages reach `maxEmptyPages`.
486
+ - The operation fails.
487
+
488
+ With `resume: true` or `--resume`, XTrawl reads a matching cursor from SQLite before the run and
489
+ updates it as pagination advances. Checkpoint identity includes the operation and normalized request,
490
+ so a materially different request starts from its own checkpoint. A successfully completed operation
491
+ clears its checkpoint.
492
+
493
+ For profile and relationship methods, `initialCursors` can provide an explicit cursor keyed by the
494
+ target identity. An explicit initial cursor takes precedence over a stored checkpoint.
495
+
496
+ ## Save output
497
+
498
+ Enable file output per operation:
499
+
500
+ ```ts
501
+ const result = await client.search("typescript", {
502
+ limit: 100,
503
+ save: true,
504
+ saveFormat: "both",
505
+ saveDir: "./exports",
506
+ saveName: "typescript-posts",
507
+ });
508
+ ```
509
+
510
+ This writes:
511
+
512
+ ```text
513
+ exports/typescript-posts.csv
514
+ exports/typescript-posts.json
515
+ ```
516
+
517
+ Without overrides, the directory is `outputs`, the format is `csv`, and the base name reflects the
518
+ query/date range or operation targets. Existing CSV and JSON files are appended instead of replaced.
519
+
520
+ JSON preserves the normalized records. Tweet CSV output flattens the common post fields, including
521
+ the post ID, timestamp, author, text, engagement counts, URL, and media links.
522
+
523
+ ## Configure XTrawl
524
+
525
+ Pass configuration fields to `XTrawl.create()` or the constructor. Common defaults are:
526
+
527
+ | Option | Default | Purpose |
528
+ | --- | --- | --- |
529
+ | `dbPath` | `xtrawl_state.db` | SQLite account and run state |
530
+ | `concurrency` | `5` | Configured worker count |
531
+ | `saveDir` | `outputs` | File output directory |
532
+ | `saveFormat` | `csv` | File output format |
533
+ | `apiPageSize` | `20` | Requested records per API page |
534
+ | `searchSplits` | `5` | Maximum date intervals per search |
535
+ | `schedulerMinIntervalMs` | `300000` | Smallest search interval |
536
+ | `maxEmptyPages` | `1` | Consecutive empty-page stop threshold |
537
+ | `dailyRequestsLimit` | `30` | Per-account daily operation guard |
538
+ | `dailyTweetsLimit` | `600` | Per-account daily collected-post guard |
539
+ | `cooldownDefaultMs` | `120000` | Default rate-limit cooldown |
540
+ | `transientCooldownMs` | `120000` | Network and transient cooldown |
541
+ | `leaseTtlMs` | `120000` | Account lease lifetime |
542
+ | `leaseHeartbeatMs` | `30000` | Active lease renewal interval |
543
+ | `requestsPerMinute` | `30` | Per-account token-bucket rate |
544
+ | `minDelayMs` | `2000` | Minimum spacing between account requests |
545
+ | `maxTaskAttempts` | `3` | Attempts for a failed page request |
546
+ | `maxAccountSwitches` | `2` | Account changes allowed within one page request |
547
+ | `proxyCheckOnLease` | `true` | Check a configured proxy before use |
548
+ | `proxyCheckTimeoutMs` | `10000` | Proxy health-check timeout |
549
+ | `manifestTtlMs` | `3600000` | Cached remote manifest lifetime |
550
+ | `manifestUpdateOnInit` | `false` | Force configured manifest URL refresh on first use |
551
+ | `manifestScrapeOnInit` | `false` | Enable live operation-identifier refresh |
552
+ | `transactionIdEnabled` | `true` | Generate current web transaction headers when possible |
553
+ | `transactionIdTtlMs` | `21600000` | Reuse transaction bootstrap material for six hours |
554
+ | `strict` | `false` | Fail a multi-target run when any task fails |
555
+
556
+ Limits are local safeguards, not statements about the platform's actual limits. XTrawl validates
557
+ configuration before opening a live operation; positive fields must be valid positive numbers and
558
+ `apiPageSize` cannot exceed 100.
559
+
560
+ ## Understand return types
561
+
562
+ ### `SearchResult`
563
+
564
+ Search and profile timeline methods return:
565
+
566
+ ```ts
567
+ interface SearchResult {
568
+ readonly tweets: readonly TweetRecord[];
569
+ readonly stats: RunStats;
570
+ }
571
+ ```
572
+
573
+ `TweetRecord` includes the post ID, author, timestamp, text, engagement counts, image links, post URL,
574
+ and optional raw source data when available. `RunStats` reports collected count, task counts,
575
+ failures, and retries.
576
+
577
+ ### `ProfileRecord`
578
+
579
+ Profile lookup returns normalized identity, biography, location, account creation time, public
580
+ counts, verification flags, protection status, profile images, banner, URL, and optional raw data.
581
+
582
+ ### `FollowRecord`
583
+
584
+ Relationship methods return profile fields plus the relationship type and the original target. The
585
+ type is `followers`, `following`, or `verified_followers`.
586
+
587
+ All response shapes are exported from the package entry point as TypeScript types.
588
+
589
+ ## Handle errors
590
+
591
+ Public failures extend `XTrawlError` and expose a `code` plus structured diagnostics:
592
+
593
+ ```ts
594
+ import {
595
+ AuthError,
596
+ RateLimitError,
597
+ XTrawlError,
598
+ } from "xtrawl";
599
+
600
+ try {
601
+ await client.search("typescript", { limit: 100 });
602
+ } catch (error) {
603
+ if (error instanceof RateLimitError) {
604
+ console.error("The active account was rate limited.");
605
+ } else if (error instanceof AuthError) {
606
+ console.error("Refresh the account cookies.");
607
+ } else if (error instanceof XTrawlError) {
608
+ console.error(error.code, error.message, error.diagnostics);
609
+ } else {
610
+ throw error;
611
+ }
612
+ }
613
+ ```
614
+
615
+ Exported error classes include:
616
+
617
+ - `ConfigError` for invalid configuration
618
+ - `ManifestError` for invalid or unavailable operation manifests
619
+ - `AccountPoolExhausted` when no account is eligible
620
+ - `AuthError` for rejected authentication
621
+ - `RateLimitError` for platform rate limits
622
+ - `NetworkError` and `ProxyError` for transport failures
623
+ - `EngineError` and `RunFailed` for operation-level failures
624
+ - `ResumeError` for invalid checkpoint state
625
+
626
+ Do not add raw tokens or cookie values to application logs when handling an error.
627
+
628
+ ## Refresh operation identifiers
629
+
630
+ X's web operation identifiers can change. XTrawl automatically refreshes the authenticated web
631
+ manifest and retries once when X rejects an outdated operation ID with HTTP 404 or 422. You can also
632
+ refresh before the first operation:
633
+
634
+ ```bash
635
+ xtrawl --manifest-scrape-on-init search "typescript" --limit 20
636
+ ```
637
+
638
+ Or in TypeScript:
639
+
640
+ ```ts
641
+ const client = await XTrawl.create({
642
+ authToken: process.env.X_AUTH_TOKEN,
643
+ csrfToken: process.env.X_CSRF_TOKEN,
644
+ manifestScrapeOnInit: true,
645
+ });
646
+ ```
647
+
648
+ Refresh reads the authenticated responsive-web main bundle, accepts only supported X script hosts,
649
+ and requires at least one real operation match. If an optional startup refresh fails, XTrawl falls
650
+ back to the bundled manifest. A configured `manifestUrl` can also provide a remote JSON manifest;
651
+ XTrawl caches it in SQLite and can use a stale cached value when a refresh fails.
652
+
653
+ ## Understand storage and account health
654
+
655
+ XTrawl uses SQLite for operational state:
656
+
657
+ - Provisioned accounts and their health status
658
+ - Exclusive account leases and lease expiry
659
+ - Daily request and collected-post counters
660
+ - Cooldown and last-error information
661
+ - Run status and failure summaries
662
+ - Pagination checkpoints
663
+ - Cached operation manifests
664
+
665
+ Before a request, the account pool selects an account that has authentication material, is not
666
+ leased, is not cooling down, and remains within configured local limits. After the operation:
667
+
668
+ - A successful account returns to the eligible pool.
669
+ - A rate-limit, network, proxy, or transient failure applies the corresponding cooldown.
670
+ - An authentication rejection marks the account unusable so it is not selected again.
671
+
672
+ SQLite coordinates account leases so separate work does not intentionally use the same stored
673
+ account at the same time.
674
+
675
+ ## Manage local state
676
+
677
+ `client.db` provides scoped operational maintenance without exposing the storage implementation:
678
+
679
+ ```ts
680
+ console.log(client.db.accountsSummary());
681
+ console.log(client.db.listAccounts({ eligibleOnly: true }));
682
+
683
+ client.db.setAccountProxy("collector-one", "socks5://127.0.0.1:1080");
684
+ await client.db.repairAccount("collector-one", true);
685
+ client.db.resetAccountCooldowns(["collector-one"], true);
686
+ client.db.clearLeases(true);
687
+ client.db.resetDailyCounters();
688
+
689
+ console.log(client.db.lastRun());
690
+ console.log(client.db.runsSummary());
691
+ client.db.clearAllCheckpoints();
692
+ ```
693
+
694
+ Account listings are redacted by default. `importAccounts()` accepts the same inline and file inputs
695
+ as client provisioning. `deleteAccount(username)` deletes only the named row.
696
+ `collapseDuplicateAccounts()` reports what it would remove; pass `false` only when you explicitly
697
+ want to merge and delete duplicate token rows.
698
+
699
+ ## Protect credentials and collected data
700
+
701
+ Treat the following as sensitive:
702
+
703
+ - `auth_token`, `ct0`, bearer overrides, and complete cookie jars
704
+ - SQLite state files containing provisioned account records
705
+ - Proxy URLs containing usernames or passwords
706
+ - Collected output that may contain personal data
707
+
708
+ Follow these rules:
709
+
710
+ 1. Keep secrets in environment variables or a caller-owned secret store.
711
+ 2. Never commit `.env` files, account files, SQLite databases, or real response fixtures.
712
+ 3. Use dedicated accounts that you own or are explicitly authorized to operate.
713
+ 4. Restrict filesystem permissions and retention for state and output files.
714
+ 5. Keep even redacted diagnostics and `inspect()` output within trusted operational tooling.
715
+ 6. Collect only what you need and follow platform terms and applicable law.
716
+
717
+ XTrawl sends read-only HTTP requests. Its transport does not implement posting, replying, liking,
718
+ following, messaging, or account-administration operations.
719
+
720
+ ## Troubleshoot common problems
721
+
722
+ ### No eligible account is available
723
+
724
+ Check that at least one account has both usable `auth_token` and `ct0` values. The account may also be
725
+ leased, cooling down, marked unusable, or over a configured daily guard. Use `client.inspect()` in a
726
+ trusted local process to review account status.
727
+
728
+ ### Authentication fails
729
+
730
+ Refresh both browser session cookies and use a fresh state database or deliberately reset the stored
731
+ account state before provisioning them again. Confirm they came from the same authorized session.
732
+ Authentication failures mark the stored account unusable.
733
+
734
+ ### Search returns no posts
735
+
736
+ Start with a simpler query, remove restrictive filters, and use `displayType: "Latest"`. An empty page
737
+ can also mean the active account cannot view the requested content.
738
+
739
+ ### A run stops early
740
+
741
+ Review `limit`, `perProfileLimit`, `maxPagesPerProfile`, and `maxEmptyPages`. The endpoint may also
742
+ have returned no next cursor. Enable `resume` before long runs so interrupted pagination can continue.
743
+
744
+ ### An endpoint suddenly fails
745
+
746
+ XTrawl retries once with an authenticated manifest refresh after HTTP 404 or 422. If the retry also
747
+ fails, the endpoint path, variables, or response shape may have changed and code changes may be
748
+ required.
749
+
750
+ ### The CLI treats a global option as a command option
751
+
752
+ Move global options before the command:
753
+
754
+ ```bash
755
+ # Correct
756
+ xtrawl --db-path ./state/xtrawl.db search "typescript" --limit 20
757
+ ```
758
+
759
+ ## Know the limitations
760
+
761
+ - XTrawl collects only content visible to the authenticated session. It does not bypass protected or
762
+ private accounts.
763
+ - The project depends on undocumented X web GraphQL endpoints, response shapes, and query IDs. They
764
+ can change without notice.
765
+ - Browser cookies expire and may be invalidated by account security events.
766
+ - Local cooldowns, delays, and usage guards cannot guarantee account availability or uninterrupted
767
+ collection.
768
+ - Pagination completeness depends on the cursors and records returned by the platform.
769
+ - Live integration tests require caller-supplied credentials and do not run in the default test gate.
770
+
771
+ For the behavioral contract, architecture boundaries, and security decisions, read the
772
+ [product specification](docs/product/specification.md),
773
+ [architecture overview](docs/architecture/overview.md), and
774
+ [security and data boundary](docs/security/data-boundary.md).