@xpoz/xpoz 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.
package/README.md ADDED
@@ -0,0 +1,663 @@
1
+ # Xpoz TypeScript SDK
2
+
3
+ TypeScript SDK for the [Xpoz](https://xpoz.ai) social media intelligence platform. Query Twitter/X, Instagram, and Reddit data through a simple, typed interface.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install xpoz
9
+ ```
10
+
11
+ Requires Node.js 18+.
12
+
13
+ ## Get an API Key
14
+
15
+ Sign up and get your token at **https://xpoz.ai/get-token**.
16
+
17
+ Once you have it, pass it directly or set the `XPOZ_API_KEY` environment variable:
18
+
19
+ ```bash
20
+ export XPOZ_API_KEY=your-token-here
21
+ ```
22
+
23
+ ## What is Xpoz?
24
+
25
+ Xpoz provides unified access to social media data across Twitter/X, Instagram, and Reddit. The platform indexes billions of posts, user profiles, and engagement metrics — making it possible to search, analyze, and export social media data at scale.
26
+
27
+ The SDK wraps Xpoz's [MCP](https://modelcontextprotocol.io) server, abstracting away transport, authentication, operation polling, and pagination into a clean developer-friendly API.
28
+
29
+ ## Features
30
+
31
+ - **30 data methods** across Twitter, Instagram, and Reddit
32
+ - **Fully async** — all methods return `Promise<T>`
33
+ - **Automatic operation polling** — long-running queries are abstracted away
34
+ - **Server-side pagination** — `PaginatedResult<T>` with `nextPage()`, `getPage(n)`
35
+ - **CSV export** — `exportCsv()` on any paginated result
36
+ - **Field selection** — request only the fields you need
37
+ - **TypeScript-first** — fully typed results with autocomplete support
38
+ - **Namespaced API** — `client.twitter.*`, `client.instagram.*`, `client.reddit.*`
39
+
40
+ ## Quick Start
41
+
42
+ ```typescript
43
+ import { XpozClient } from "xpoz";
44
+
45
+ const client = new XpozClient({ apiKey: "your-api-key" });
46
+ await client.connect();
47
+
48
+ const user = await client.twitter.getUser("elonmusk");
49
+ console.log(`${user.name} — ${user.followersCount?.toLocaleString()} followers`);
50
+
51
+ const results = await client.twitter.searchPosts("artificial intelligence", {
52
+ startDate: "2025-01-01",
53
+ });
54
+ for (const post of results.data) {
55
+ console.log(post.text, post.likeCount);
56
+ }
57
+
58
+ await client.close();
59
+ ```
60
+
61
+ ## Authentication
62
+
63
+ Get your API key at https://xpoz.ai/get-token, then use it as follows:
64
+
65
+ ```typescript
66
+ // Pass API key directly
67
+ const client = new XpozClient({ apiKey: "your-api-key" });
68
+
69
+ // Or use XPOZ_API_KEY environment variable
70
+ const client = new XpozClient();
71
+
72
+ // Custom server URL (also reads XPOZ_SERVER_URL env var)
73
+ const client = new XpozClient({ apiKey: "your-api-key", serverUrl: "https://xpoz.ai/mcp" });
74
+
75
+ // Custom operation timeout in milliseconds (default: 300000)
76
+ const client = new XpozClient({ apiKey: "your-api-key", timeoutMs: 600_000 });
77
+ ```
78
+
79
+ ## Async Disposal
80
+
81
+ ```typescript
82
+ // Using Symbol.asyncDispose (Node.js 18.2+ with --experimental-vm-modules or TypeScript 5.2+)
83
+ await using client = new XpozClient({ apiKey: "your-api-key" });
84
+ await client.connect();
85
+ const user = await client.twitter.getUser("elonmusk");
86
+ // client.close() is called automatically
87
+
88
+ // Manual connect/close
89
+ const client = new XpozClient({ apiKey: "your-api-key" });
90
+ await client.connect();
91
+ try {
92
+ const results = await client.twitter.searchPosts("AI");
93
+ } finally {
94
+ await client.close();
95
+ }
96
+ ```
97
+
98
+ ## Pagination
99
+
100
+ Methods that return large datasets use server-side pagination (100 items per page). These return a `PaginatedResult<T>` with built-in helpers:
101
+
102
+ ```typescript
103
+ const results = await client.twitter.searchPosts("AI");
104
+
105
+ results.data // TwitterPost[] — current page
106
+ results.pagination.totalRows // total matching rows
107
+ results.pagination.totalPages // total pages
108
+ results.pagination.pageNumber // current page number
109
+ results.pagination.pageSize // items per page (100)
110
+ results.pagination.resultsCount // items on current page
111
+ results.hasNextPage() // boolean
112
+
113
+ // Navigate pages
114
+ const page2 = await results.nextPage(); // fetch next page
115
+ const page5 = await results.getPage(5); // jump to specific page
116
+
117
+ // Export to CSV
118
+ const csvUrl = await results.exportCsv(); // returns download URL
119
+ ```
120
+
121
+ ## Field Selection
122
+
123
+ All methods accept a `fields` option. Use camelCase field names.
124
+
125
+ ```typescript
126
+ // Only fetch the fields you need (faster + less memory)
127
+ const results = await client.twitter.searchPosts("AI", {
128
+ fields: ["id", "text", "likeCount", "retweetCount", "createdAtDate"],
129
+ });
130
+
131
+ const user = await client.twitter.getUser("elonmusk", {
132
+ fields: ["id", "username", "name", "followersCount", "description"],
133
+ });
134
+ ```
135
+
136
+ Requesting fewer fields significantly improves response time.
137
+
138
+ ## Query Syntax
139
+
140
+ The `query` parameter on all `search*` and `get*ByKeywords` methods supports a Lucene-style full-text syntax across Twitter, Instagram, and Reddit.
141
+
142
+ ### Exact phrase
143
+ Wrap in double quotes to require an exact match:
144
+ ```
145
+ "machine learning"
146
+ "climate change"
147
+ ```
148
+
149
+ ### Keywords (any word)
150
+ Space-separated terms without quotes match posts containing **any** of the words:
151
+ ```
152
+ AI crypto blockchain
153
+ ```
154
+
155
+ ### Boolean operators
156
+ Use `AND`, `OR`, `NOT` (case-insensitive). A bare space is treated as `OR` — be explicit:
157
+ ```
158
+ "deep learning" AND python
159
+ tensorflow OR pytorch
160
+ climate NOT politics
161
+ ```
162
+
163
+ ### Grouping with parentheses
164
+ ```
165
+ (AI OR "artificial intelligence") AND ethics
166
+ (startup OR entrepreneur) NOT "venture capital"
167
+ ```
168
+
169
+ ### Combined example
170
+ ```typescript
171
+ const results = await client.twitter.searchPosts(
172
+ '("machine learning" OR "deep learning") AND python NOT spam',
173
+ {
174
+ startDate: "2025-01-01",
175
+ language: "en",
176
+ }
177
+ );
178
+ ```
179
+
180
+ > **Note:** Do not use `from:`, `lang:`, `since:`, or `until:` in the query string — use the dedicated parameters (`authorUsername`, `language`, `startDate`, `endDate`) instead.
181
+
182
+ ## Error Handling
183
+
184
+ ```typescript
185
+ import {
186
+ XpozError,
187
+ AuthenticationError,
188
+ XpozConnectionError,
189
+ OperationTimeoutError,
190
+ OperationFailedError,
191
+ OperationCancelledError,
192
+ } from "xpoz";
193
+
194
+ try {
195
+ const user = await client.twitter.getUser("nonexistent_user_12345");
196
+ } catch (e) {
197
+ if (e instanceof OperationFailedError) {
198
+ console.log(`Operation ${e.operationId} failed: ${e.operationError}`);
199
+ } else if (e instanceof OperationTimeoutError) {
200
+ console.log(`Timed out after ${Math.round(e.elapsedMs / 1000)}s`);
201
+ } else if (e instanceof AuthenticationError) {
202
+ console.log("Invalid API key");
203
+ } else if (e instanceof XpozError) {
204
+ console.log(`Xpoz error: ${e.message}`);
205
+ }
206
+ }
207
+ ```
208
+
209
+ ---
210
+
211
+ ## API Reference
212
+
213
+ ### Twitter — `client.twitter`
214
+
215
+ #### `getUser(identifier, options?) -> Promise<TwitterUser>`
216
+
217
+ Get a single Twitter user profile.
218
+
219
+ ```typescript
220
+ // By username (default)
221
+ const user = await client.twitter.getUser("elonmusk");
222
+
223
+ // By numeric ID
224
+ const user = await client.twitter.getUser("44196397", { identifierType: "id" });
225
+ ```
226
+
227
+ #### `searchUsers(name, options?) -> Promise<TwitterUser[]>`
228
+
229
+ Search users by name or username. Returns up to 10 results.
230
+
231
+ ```typescript
232
+ const users = await client.twitter.searchUsers("elon");
233
+ ```
234
+
235
+ #### `getUserConnections(username, connectionType, options?) -> Promise<PaginatedResult<TwitterUser>>`
236
+
237
+ Get followers or following for a user.
238
+
239
+ ```typescript
240
+ const followers = await client.twitter.getUserConnections("elonmusk", "followers");
241
+ const following = await client.twitter.getUserConnections("elonmusk", "following");
242
+ ```
243
+
244
+ #### `getUsersByKeywords(query, options?) -> Promise<PaginatedResult<TwitterUser>>`
245
+
246
+ Find users who authored posts matching a keyword query.
247
+
248
+ ```typescript
249
+ const users = await client.twitter.getUsersByKeywords('"machine learning"', {
250
+ fields: ["username", "name", "followersCount"],
251
+ });
252
+ ```
253
+
254
+ #### `getPostsByIds(postIds, options?) -> Promise<TwitterPost[]>`
255
+
256
+ Get 1-100 posts by their IDs.
257
+
258
+ ```typescript
259
+ const tweets = await client.twitter.getPostsByIds(["1234567890", "0987654321"]);
260
+ ```
261
+
262
+ #### `getPostsByAuthor(identifier, options?) -> Promise<PaginatedResult<TwitterPost>>`
263
+
264
+ Get all posts by an author with optional date filtering.
265
+
266
+ ```typescript
267
+ const results = await client.twitter.getPostsByAuthor("elonmusk", {
268
+ startDate: "2025-01-01",
269
+ });
270
+ ```
271
+
272
+ #### `searchPosts(query, options?) -> Promise<PaginatedResult<TwitterPost>>`
273
+
274
+ Full-text search with filters. Supports exact phrases (`"machine learning"`), boolean operators (`AI AND python`), and parentheses.
275
+
276
+ ```typescript
277
+ const results = await client.twitter.searchPosts('"artificial intelligence" AND ethics', {
278
+ startDate: "2025-01-01",
279
+ endDate: "2025-06-01",
280
+ language: "en",
281
+ fields: ["id", "text", "likeCount", "authorUsername", "createdAtDate"],
282
+ });
283
+ ```
284
+
285
+ #### `getRetweets(postId, options?) -> Promise<PaginatedResult<TwitterPost>>`
286
+
287
+ Get retweets of a specific post (database only).
288
+
289
+ ```typescript
290
+ const retweets = await client.twitter.getRetweets("1234567890");
291
+ ```
292
+
293
+ #### `getQuotes(postId, options?) -> Promise<PaginatedResult<TwitterPost>>`
294
+
295
+ Get quote tweets of a specific post.
296
+
297
+ ```typescript
298
+ const quotes = await client.twitter.getQuotes("1234567890");
299
+ ```
300
+
301
+ #### `getComments(postId, options?) -> Promise<PaginatedResult<TwitterPost>>`
302
+
303
+ Get replies to a specific post.
304
+
305
+ ```typescript
306
+ const comments = await client.twitter.getComments("1234567890");
307
+ ```
308
+
309
+ #### `getPostInteractingUsers(postId, interactionType, options?) -> Promise<PaginatedResult<TwitterUser>>`
310
+
311
+ Get users who interacted with a post. `interactionType`: `"commenters"`, `"quoters"`, `"retweeters"`.
312
+
313
+ ```typescript
314
+ const commenters = await client.twitter.getPostInteractingUsers("1234567890", "commenters");
315
+ ```
316
+
317
+ #### `countPosts(phrase, options?) -> Promise<number>`
318
+
319
+ Count tweets containing a phrase within a date range.
320
+
321
+ ```typescript
322
+ const count = await client.twitter.countPosts("bitcoin", { startDate: "2025-01-01" });
323
+ console.log(`${count.toLocaleString()} tweets mention bitcoin`);
324
+ ```
325
+
326
+ ---
327
+
328
+ ### Instagram — `client.instagram`
329
+
330
+ #### `getUser(identifier, options?) -> Promise<InstagramUser>`
331
+
332
+ ```typescript
333
+ const user = await client.instagram.getUser("instagram");
334
+ console.log(`${user.fullName} — ${user.followerCount?.toLocaleString()} followers`);
335
+ ```
336
+
337
+ #### `searchUsers(name, options?) -> Promise<InstagramUser[]>`
338
+
339
+ ```typescript
340
+ const users = await client.instagram.searchUsers("nasa");
341
+ ```
342
+
343
+ #### `getUserConnections(username, connectionType, options?) -> Promise<PaginatedResult<InstagramUser>>`
344
+
345
+ ```typescript
346
+ const followers = await client.instagram.getUserConnections("instagram", "followers");
347
+ ```
348
+
349
+ #### `getUsersByKeywords(query, options?) -> Promise<PaginatedResult<InstagramUser>>`
350
+
351
+ ```typescript
352
+ const users = await client.instagram.getUsersByKeywords('"sustainable fashion"');
353
+ ```
354
+
355
+ #### `getPostsByIds(postIds, options?) -> Promise<InstagramPost[]>`
356
+
357
+ Post IDs must be in strong_id format: `"media_id_user_id"` (e.g. `"3606450040306139062_4836333238"`).
358
+
359
+ ```typescript
360
+ const posts = await client.instagram.getPostsByIds(["3606450040306139062_4836333238"]);
361
+ ```
362
+
363
+ #### `getPostsByUser(identifier, options?) -> Promise<PaginatedResult<InstagramPost>>`
364
+
365
+ ```typescript
366
+ const results = await client.instagram.getPostsByUser("nasa");
367
+ ```
368
+
369
+ #### `searchPosts(query, options?) -> Promise<PaginatedResult<InstagramPost>>`
370
+
371
+ ```typescript
372
+ const results = await client.instagram.searchPosts("travel photography");
373
+ ```
374
+
375
+ #### `getComments(postId, options?) -> Promise<PaginatedResult<InstagramComment>>`
376
+
377
+ ```typescript
378
+ const comments = await client.instagram.getComments("3606450040306139062_4836333238");
379
+ ```
380
+
381
+ #### `getPostInteractingUsers(postId, interactionType, options?) -> Promise<PaginatedResult<InstagramUser>>`
382
+
383
+ `interactionType`: `"commenters"`, `"likers"`.
384
+
385
+ ```typescript
386
+ const likers = await client.instagram.getPostInteractingUsers(
387
+ "3606450040306139062_4836333238",
388
+ "likers"
389
+ );
390
+ ```
391
+
392
+ ---
393
+
394
+ ### Reddit — `client.reddit`
395
+
396
+ #### `getUser(username, options?) -> Promise<RedditUser>`
397
+
398
+ ```typescript
399
+ const user = await client.reddit.getUser("spez");
400
+ console.log(`${user.username} — ${user.totalKarma?.toLocaleString()} karma`);
401
+ ```
402
+
403
+ #### `searchUsers(name, options?) -> Promise<RedditUser[]>`
404
+
405
+ ```typescript
406
+ const users = await client.reddit.searchUsers("spez");
407
+ ```
408
+
409
+ #### `getUsersByKeywords(query, options?) -> Promise<PaginatedResult<RedditUser>>`
410
+
411
+ ```typescript
412
+ const users = await client.reddit.getUsersByKeywords('"machine learning"', {
413
+ subreddit: "MachineLearning",
414
+ });
415
+ ```
416
+
417
+ #### `searchPosts(query, options?) -> Promise<PaginatedResult<RedditPost>>`
418
+
419
+ `sort`: `"relevance"`, `"hot"`, `"top"`, `"new"`, `"comments"`. `time`: `"hour"`, `"day"`, `"week"`, `"month"`, `"year"`, `"all"`.
420
+
421
+ ```typescript
422
+ const results = await client.reddit.searchPosts("python tutorial", {
423
+ subreddit: "learnpython",
424
+ sort: "top",
425
+ time: "month",
426
+ });
427
+ ```
428
+
429
+ #### `getPostWithComments(postId, options?) -> Promise<RedditPostWithComments>`
430
+
431
+ Returns an object with the post and its comments.
432
+
433
+ ```typescript
434
+ const result = await client.reddit.getPostWithComments("abc123");
435
+ console.log(result.post.title);
436
+ for (const comment of result.comments) {
437
+ console.log(` ${comment.authorUsername}: ${comment.body?.slice(0, 80)}`);
438
+ }
439
+ ```
440
+
441
+ #### `searchComments(query, options?) -> Promise<PaginatedResult<RedditComment>>`
442
+
443
+ ```typescript
444
+ const comments = await client.reddit.searchComments("helpful tip", {
445
+ subreddit: "LifeProTips",
446
+ });
447
+ ```
448
+
449
+ #### `searchSubreddits(query, options?) -> Promise<RedditSubreddit[]>`
450
+
451
+ ```typescript
452
+ const subs = await client.reddit.searchSubreddits("machine learning");
453
+ ```
454
+
455
+ #### `getSubredditWithPosts(subredditName, options?) -> Promise<SubredditWithPosts>`
456
+
457
+ ```typescript
458
+ const result = await client.reddit.getSubredditWithPosts("wallstreetbets");
459
+ console.log(`r/${result.subreddit.displayName} — ${result.subreddit.subscribersCount?.toLocaleString()} members`);
460
+ for (const post of result.posts) {
461
+ console.log(` ${post.title} (${post.score} points)`);
462
+ }
463
+ ```
464
+
465
+ #### `getSubredditsByKeywords(query, options?) -> Promise<PaginatedResult<RedditSubreddit>>`
466
+
467
+ ```typescript
468
+ const subs = await client.reddit.getSubredditsByKeywords("cryptocurrency");
469
+ ```
470
+
471
+ ---
472
+
473
+ ## Type Models
474
+
475
+ All fields are optional and typed as their respective TypeScript types. Unknown fields are preserved on the object.
476
+
477
+ ### TwitterPost
478
+
479
+ | Field | Type | Description |
480
+ | ------------------- | ---------- | -------------------------- |
481
+ | `id` | `string` | Post ID |
482
+ | `text` | `string` | Post text content |
483
+ | `authorId` | `string` | Author's user ID |
484
+ | `authorUsername` | `string` | Author's username |
485
+ | `likeCount` | `number` | Number of likes |
486
+ | `retweetCount` | `number` | Number of retweets |
487
+ | `replyCount` | `number` | Number of replies |
488
+ | `quoteCount` | `number` | Number of quotes |
489
+ | `impressionCount` | `number` | Number of impressions |
490
+ | `bookmarkCount` | `number` | Number of bookmarks |
491
+ | `lang` | `string` | Language code |
492
+ | `hashtags` | `string[]` | Hashtags in tweet |
493
+ | `mentions` | `string[]` | Mentioned usernames |
494
+ | `mediaUrls` | `string[]` | Media attachment URLs |
495
+ | `urls` | `string[]` | URLs in tweet |
496
+ | `country` | `string` | Country (if geo-tagged) |
497
+ | `createdAt` | `string` | Creation timestamp |
498
+ | `createdAtDate` | `string` | Creation date (YYYY-MM-DD) |
499
+ | `conversationId` | `string` | Thread conversation ID |
500
+ | `quotedTweetId` | `string` | ID of quoted tweet |
501
+ | `replyToTweetId` | `string` | ID of parent tweet |
502
+ | `isRetweet` | `boolean` | Whether this is a retweet |
503
+ | `possiblySensitive` | `boolean` | Sensitive content flag |
504
+
505
+ ### TwitterUser
506
+
507
+ | Field | Type | Description |
508
+ | ---------------------------- | --------- | -------------------------- |
509
+ | `id` | `string` | User ID |
510
+ | `username` | `string` | Username (handle) |
511
+ | `name` | `string` | Display name |
512
+ | `description` | `string` | Bio text |
513
+ | `location` | `string` | Location string |
514
+ | `verified` | `boolean` | Verification status |
515
+ | `verifiedType` | `string` | Verification type |
516
+ | `followersCount` | `number` | Number of followers |
517
+ | `followingCount` | `number` | Number of following |
518
+ | `tweetCount` | `number` | Total tweets |
519
+ | `likesCount` | `number` | Total likes |
520
+ | `profileImageUrl` | `string` | Profile picture URL |
521
+ | `createdAt` | `string` | Account creation timestamp |
522
+ | `accountBasedIn` | `string` | Account location |
523
+ | `isInauthentic` | `boolean` | Inauthenticity flag |
524
+ | `isInauthenticProbScore` | `number` | Inauthenticity probability |
525
+ | `avgTweetsPerDayLastMonth` | `number` | Tweeting frequency |
526
+
527
+ ### InstagramPost
528
+
529
+ | Field | Type | Description |
530
+ | ---------------- | -------- | -------------------------- |
531
+ | `id` | `string` | Post ID (strong_id format) |
532
+ | `caption` | `string` | Post caption |
533
+ | `username` | `string` | Author username |
534
+ | `fullName` | `string` | Author display name |
535
+ | `likeCount` | `number` | Number of likes |
536
+ | `commentCount` | `number` | Number of comments |
537
+ | `reshareCount` | `number` | Number of reshares |
538
+ | `videoPlayCount` | `number` | Video play count |
539
+ | `mediaType` | `string` | Media type |
540
+ | `imageUrl` | `string` | Image URL |
541
+ | `videoUrl` | `string` | Video URL |
542
+ | `createdAtDate` | `string` | Creation date |
543
+
544
+ ### InstagramUser
545
+
546
+ | Field | Type | Description |
547
+ | ---------------- | --------- | ------------------- |
548
+ | `id` | `string` | User ID |
549
+ | `username` | `string` | Username |
550
+ | `fullName` | `string` | Display name |
551
+ | `biography` | `string` | Bio text |
552
+ | `isPrivate` | `boolean` | Private account |
553
+ | `isVerified` | `boolean` | Verified status |
554
+ | `followerCount` | `number` | Followers |
555
+ | `followingCount` | `number` | Following |
556
+ | `mediaCount` | `number` | Total posts |
557
+ | `profilePicUrl` | `string` | Profile picture URL |
558
+
559
+ ### InstagramComment
560
+
561
+ | Field | Type | Description |
562
+ | ------------------- | -------- | --------------- |
563
+ | `id` | `string` | Comment ID |
564
+ | `text` | `string` | Comment text |
565
+ | `username` | `string` | Author username |
566
+ | `parentPostId` | `string` | Parent post ID |
567
+ | `likeCount` | `number` | Number of likes |
568
+ | `childCommentCount` | `number` | Reply count |
569
+ | `createdAtDate` | `string` | Creation date |
570
+
571
+ ### RedditPost
572
+
573
+ | Field | Type | Description |
574
+ | ---------------- | --------- | --------------------- |
575
+ | `id` | `string` | Post ID |
576
+ | `title` | `string` | Post title |
577
+ | `selftext` | `string` | Post body text |
578
+ | `authorUsername` | `string` | Author username |
579
+ | `subredditName` | `string` | Subreddit name |
580
+ | `score` | `number` | Net score |
581
+ | `upvotes` | `number` | Upvote count |
582
+ | `commentsCount` | `number` | Comment count |
583
+ | `url` | `string` | Post URL |
584
+ | `permalink` | `string` | Reddit permalink |
585
+ | `isSelf` | `boolean` | Self post (text only) |
586
+ | `over18` | `boolean` | NSFW flag |
587
+ | `createdAtDate` | `string` | Creation date |
588
+
589
+ ### RedditUser
590
+
591
+ | Field | Type | Description |
592
+ | -------------------- | --------- | --------------------- |
593
+ | `id` | `string` | User ID |
594
+ | `username` | `string` | Username |
595
+ | `totalKarma` | `number` | Total karma |
596
+ | `linkKarma` | `number` | Link karma |
597
+ | `commentKarma` | `number` | Comment karma |
598
+ | `isGold` | `boolean` | Reddit Gold status |
599
+ | `isMod` | `boolean` | Moderator status |
600
+ | `profileDescription` | `string` | Profile bio |
601
+ | `createdAtDate` | `string` | Account creation date |
602
+
603
+ ### RedditComment
604
+
605
+ | Field | Type | Description |
606
+ | ---------------- | --------- | --------------- |
607
+ | `id` | `string` | Comment ID |
608
+ | `body` | `string` | Comment text |
609
+ | `authorUsername` | `string` | Author username |
610
+ | `parentPostId` | `string` | Parent post ID |
611
+ | `score` | `number` | Net score |
612
+ | `depth` | `number` | Nesting depth |
613
+ | `isSubmitter` | `boolean` | Is OP |
614
+ | `createdAtDate` | `string` | Creation date |
615
+
616
+ ### RedditSubreddit
617
+
618
+ | Field | Type | Description |
619
+ | ------------------- | --------- | ----------------- |
620
+ | `id` | `string` | Subreddit ID |
621
+ | `displayName` | `string` | Subreddit name |
622
+ | `title` | `string` | Subreddit title |
623
+ | `publicDescription` | `string` | Short description |
624
+ | `description` | `string` | Full description |
625
+ | `subscribersCount` | `number` | Subscriber count |
626
+ | `activeUserCount` | `number` | Active users |
627
+ | `over18` | `boolean` | NSFW flag |
628
+ | `createdAtDate` | `string` | Creation date |
629
+
630
+ ### Composite Types
631
+
632
+ **`RedditPostWithComments`** — returned by `getPostWithComments()`:
633
+
634
+ - `post: RedditPost`
635
+ - `comments: RedditComment[]`
636
+ - `commentsPagination: PaginationInfo | null`
637
+
638
+ **`SubredditWithPosts`** — returned by `getSubredditWithPosts()`:
639
+
640
+ - `subreddit: RedditSubreddit`
641
+ - `posts: RedditPost[]`
642
+ - `postsPagination: PaginationInfo | null`
643
+
644
+ ---
645
+
646
+ ## Environment Variables
647
+
648
+ | Variable | Description | Default |
649
+ | ----------------- | -------------------------- | -------------------------- |
650
+ | `XPOZ_API_KEY` | API key for authentication | — |
651
+ | `XPOZ_SERVER_URL` | MCP server URL | `https://mcp.xpoz.ai/mcp` |
652
+
653
+ ## Testing
654
+
655
+ Tests hit the live Xpoz API and require a valid API key:
656
+
657
+ ```bash
658
+ XPOZ_API_KEY=your-api-key npx vitest run
659
+ ```
660
+
661
+ ## License
662
+
663
+ MIT