@recursiv/sdk 0.7.50 → 0.7.51

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.
@@ -11,6 +11,14 @@ export class WalletResource {
11
11
  getBalance() {
12
12
  return this.client.get('/wallet/balance');
13
13
  }
14
+ /** The current user's MINDS token balance (off-chain ledger) and linked wallets. */
15
+ getTokenBalance() {
16
+ return this.client.get('/wallet/token-balance');
17
+ }
18
+ /** The current user's token ledger, newest first (limit up to 200, default 50). */
19
+ getTokenLedger(params) {
20
+ return this.client.get('/wallet/token-ledger', params);
21
+ }
14
22
  /** USD payout eligibility for the current app user (Stripe Connect). */
15
23
  getPayouts() {
16
24
  return this.client.get('/wallet/payouts');
package/dist/types.d.ts CHANGED
@@ -92,7 +92,14 @@ export interface User extends UserSummary {
92
92
  posts_count: number;
93
93
  created_at: string;
94
94
  }
95
- export interface FollowUser {
95
+ /** The caller's follow relationship with a listed account (both directions). */
96
+ export interface ViewerRelationship {
97
+ /** The caller follows this account. */
98
+ is_following: boolean;
99
+ /** This account follows the caller. */
100
+ follows_you: boolean;
101
+ }
102
+ export interface FollowUser extends ViewerRelationship {
96
103
  id: string;
97
104
  name: string;
98
105
  username: string;
@@ -155,7 +162,10 @@ export interface RepostedPost {
155
162
  created_at: string;
156
163
  }
157
164
  export interface PostDetail extends Post {
165
+ /** The newest replies (at most 50). */
158
166
  replies: PostReply[];
167
+ /** Present when the thread has more replies; pass it to `posts.replies(id, { cursor })` for the next, older page. */
168
+ replies_next_cursor?: string | null;
159
169
  }
160
170
  export interface PostReply {
161
171
  id: string;
@@ -164,8 +174,27 @@ export interface PostReply {
164
174
  author: UserSummary;
165
175
  tags: Tag[];
166
176
  reactions_count: number;
177
+ score?: number;
178
+ user_reaction?: string | null;
179
+ /** This reply's own reply count, so its sub-thread can be opened. */
180
+ reply_count?: number;
167
181
  created_at: string;
168
182
  }
183
+ export interface ListPostRepliesParams {
184
+ /** 1-100, default 50. */
185
+ limit?: number;
186
+ /** `replies_next_cursor` from `posts.get`, or `meta.next_cursor` from the previous page. */
187
+ cursor?: string;
188
+ }
189
+ /** One page of a post's replies, newest first. */
190
+ export interface PostRepliesPage {
191
+ data: PostReply[];
192
+ meta: {
193
+ limit: number;
194
+ has_more: boolean;
195
+ next_cursor: string | null;
196
+ };
197
+ }
169
198
  export interface CreatePostInput {
170
199
  content: string;
171
200
  content_format?: 'plain' | 'markdown';
@@ -214,6 +243,10 @@ export interface ListPostsParams extends PaginationParams {
214
243
  export interface SearchPostsParams extends PaginationParams {
215
244
  q: string;
216
245
  tag_ids?: string;
246
+ /** `relevance` (default) or `new` (newest first). */
247
+ sort?: 'relevance' | 'new';
248
+ since?: string;
249
+ until?: string;
217
250
  }
218
251
  export type QueryParams = Record<string, unknown>;
219
252
  export interface CommunityCreator {
@@ -232,6 +265,10 @@ export interface Community {
232
265
  privacy: 'public' | 'private' | 'hidden';
233
266
  created_by: CommunityCreator;
234
267
  member_count: number;
268
+ /** Denormalized post count. */
269
+ post_count?: number;
270
+ /** Whether the caller is an accepted member. Returned by `list` and `search`. */
271
+ is_member?: boolean;
235
272
  created_at: string;
236
273
  }
237
274
  /** Filters accepted by the communities directory. */
@@ -240,11 +277,71 @@ export interface ListCommunitiesParams extends PaginationParams {
240
277
  member?: boolean;
241
278
  /** Restrict personal keys to one organization. Project-bound keys are already scoped. */
242
279
  organization_id?: string;
243
- /** Server-side directory ordering. */
280
+ /** Server-side directory ordering. With `q`, name matches come first and this breaks ties. */
244
281
  sort?: 'members' | 'active' | 'newest';
282
+ /**
283
+ * Case-insensitive text search over name and description, up to 200
284
+ * characters. With `member: true` it searches only the caller's own groups.
285
+ */
286
+ q?: string;
245
287
  }
288
+ /** The caller's own membership row in a group, if any. */
289
+ export type CommunityMembershipStatus = 'accepted' | 'pending' | 'invited' | 'banned';
246
290
  export interface CommunityDetail extends Community {
247
291
  post_count: number;
292
+ /** The caller's role when they are an accepted member (the creator is always `owner`). */
293
+ viewer_role?: 'owner' | 'admin' | 'moderator' | 'member' | null;
294
+ /** The caller's own membership state, whether or not it is accepted. */
295
+ membership_status?: CommunityMembershipStatus | null;
296
+ /** People waiting to join. Present only for moderators and up. */
297
+ pending_request_count?: number;
298
+ /** Always absent or false on a full detail; see `CommunityCard`. */
299
+ limited?: false;
300
+ }
301
+ /**
302
+ * A private group seen from outside: `communities.get` returns this to a
303
+ * signed-in person who is not an accepted member (including a pending
304
+ * requester or a banned account), so an app can offer "Request to join" or
305
+ * show "Requested". The description, creator, roster, post count and posts
306
+ * stay with members. Hidden groups still answer 404, and so does an app's
307
+ * private group to a caller outside that app (an app key sees its own app).
308
+ */
309
+ export interface CommunityCard {
310
+ id: string;
311
+ name: string;
312
+ slug: string;
313
+ image: string | null;
314
+ banner: string | null;
315
+ privacy: 'private';
316
+ member_count: number;
317
+ is_member: false;
318
+ viewer_role: null;
319
+ membership_status: CommunityMembershipStatus | null;
320
+ limited: true;
321
+ description?: undefined;
322
+ created_by?: undefined;
323
+ post_count?: undefined;
324
+ created_at?: undefined;
325
+ }
326
+ /** A row from `communities.related`: a public group, ranked by shared members. */
327
+ export interface RelatedCommunity {
328
+ id: string;
329
+ name: string;
330
+ slug: string;
331
+ description: string | null;
332
+ image: string | null;
333
+ banner: string | null;
334
+ privacy: 'public';
335
+ /** Whether the caller is an accepted member of this related group. */
336
+ is_member: boolean;
337
+ member_count: number;
338
+ post_count: number;
339
+ /** How many members this group shares with the one asked about. */
340
+ overlap: number;
341
+ }
342
+ /** Paging plus `q`: a case-insensitive name or username search, up to 200 characters. */
343
+ export interface CommunityPeopleParams extends PaginationParams {
344
+ q?: string;
248
345
  }
249
346
  export interface CommunityMember {
250
347
  id: string;
@@ -328,8 +425,25 @@ export interface Conversation {
328
425
  last_message: LastMessage | null;
329
426
  /** Omitted by API versions deployed before message requests. */
330
427
  request_status?: ChatRequestStatus;
428
+ /** The member who started it; null when it cannot be told (older threads). */
429
+ created_by?: string | null;
430
+ /** Whether `chat.deleteConversation()` is allowed for the caller. */
431
+ can_delete?: boolean;
432
+ /** Whether `chat.leave()` is allowed for the caller. */
433
+ can_leave?: boolean;
331
434
  created_at: string;
332
435
  }
436
+ export interface ConversationRequestResult {
437
+ id: string;
438
+ request_status: ChatRequestStatus;
439
+ /** False when the conversation was already in that state. */
440
+ changed: boolean;
441
+ }
442
+ export interface LeaveConversationResult {
443
+ left: true;
444
+ /** True when the caller was the last person in it, so the room was removed. */
445
+ conversation_deleted: boolean;
446
+ }
333
447
  export interface ConversationDetail {
334
448
  /** Missing on older APIs: treat as unknown, never as plaintext permission. */
335
449
  encryption?: ConversationEncryptionPolicy;
@@ -340,6 +454,12 @@ export interface ConversationDetail {
340
454
  members: ConversationMemberDetail[];
341
455
  /** Omitted by API versions deployed before message requests. */
342
456
  request_status?: ChatRequestStatus;
457
+ /** The member who started it; null when it cannot be told (older threads). */
458
+ created_by?: string | null;
459
+ /** Whether `chat.deleteConversation()` is allowed for the caller. */
460
+ can_delete?: boolean;
461
+ /** Whether `chat.leave()` is allowed for the caller. */
462
+ can_leave?: boolean;
343
463
  created_at: string;
344
464
  }
345
465
  export interface MessageReaction {
@@ -656,6 +776,12 @@ export interface AgentChatInput {
656
776
  conversation_id?: string;
657
777
  new_conversation?: boolean;
658
778
  }
779
+ /** Answer an existing message of yours again, without sending it a second time. */
780
+ export interface AgentChatRetryInput {
781
+ conversation_id: string;
782
+ /** Your message whose reply failed (or that you want answered again). Must be the latest one. */
783
+ retry_message_id: string;
784
+ }
659
785
  export interface AgentChatResult {
660
786
  message_id: string;
661
787
  conversation_id: string;
@@ -727,6 +853,24 @@ export interface Profile {
727
853
  is_ai: boolean;
728
854
  created_at: string;
729
855
  }
856
+ /** A row of a followers/following list: the profile plus the caller's relationship with it. */
857
+ export interface FollowListProfile extends Profile, ViewerRelationship {
858
+ }
859
+ /** An account the caller blocked. */
860
+ export interface BlockedProfile extends Profile {
861
+ blocked_at: string;
862
+ }
863
+ /** An account the caller muted. */
864
+ export interface MutedProfile extends Profile {
865
+ muted_at: string;
866
+ }
867
+ /** Paging and name/username filter for the caller's blocked and muted lists. */
868
+ export type ModerationListParams = {
869
+ limit?: number;
870
+ offset?: number;
871
+ /** Case-insensitive match on name or username. */
872
+ search?: string;
873
+ };
730
874
  export interface ProfileDetail extends Profile {
731
875
  followers_count: number;
732
876
  following_count: number;
@@ -736,6 +880,12 @@ export interface ProfileMe extends Profile {
736
880
  email_verified: boolean;
737
881
  role: string;
738
882
  two_factor_enabled: boolean | null;
883
+ /**
884
+ * Whether the account can confirm changes with a password. False for
885
+ * OTP-only accounts: they confirm email and two-factor changes with a code
886
+ * from `settings.requestConfirmationCode`. Absent on servers older than this field.
887
+ */
888
+ has_password?: boolean;
739
889
  followers_count: number;
740
890
  following_count: number;
741
891
  updated_at: string;
@@ -749,6 +899,20 @@ export interface UpdateProfileInput {
749
899
  export interface SearchProfilesParams extends PaginationParams {
750
900
  q: string;
751
901
  }
902
+ export interface SearchMentionsParams {
903
+ /** What the member typed after `@` (a leading `@` is ignored). */
904
+ q: string;
905
+ /** Default 8, max 25. */
906
+ limit?: number;
907
+ [key: string]: unknown;
908
+ }
909
+ /** An account a composer's @-picker may offer: a person or an agent. */
910
+ export interface MentionCandidate extends Profile {
911
+ /** True for an agent (a platform or app agent, not a person's own AI). */
912
+ is_agent: boolean;
913
+ /** True when the caller follows this account; followed accounts rank first. */
914
+ is_following: boolean;
915
+ }
752
916
  export interface LeaderboardEntry {
753
917
  id: string;
754
918
  name: string;
@@ -759,6 +923,8 @@ export interface LeaderboardEntry {
759
923
  post_count: number;
760
924
  reaction_count: number;
761
925
  engagement: number;
926
+ /** Whether the caller follows this account. */
927
+ is_following?: boolean;
762
928
  created_at: string;
763
929
  }
764
930
  export interface Organization {
@@ -868,7 +1034,39 @@ export interface ChangePasswordInput {
868
1034
  }
869
1035
  export interface RequestEmailChangeInput {
870
1036
  new_email: string;
871
- password: string;
1037
+ /** Required for an account with a password. */
1038
+ password?: string;
1039
+ /**
1040
+ * Required for an account with no password (`has_password: false`): the
1041
+ * code from `settings.requestConfirmationCode('email_change')`, which is
1042
+ * sent to the CURRENT address.
1043
+ */
1044
+ email_code?: string;
1045
+ }
1046
+ /** What a confirmation code is for. A code for one purpose never confirms another. */
1047
+ export type ConfirmationCodePurpose = 'email_change' | 'two_factor';
1048
+ export interface ConfirmationCodeSent {
1049
+ success: true;
1050
+ purpose: ConfirmationCodePurpose;
1051
+ /** ISO timestamp. Codes live ten minutes. */
1052
+ expires_at: string;
1053
+ }
1054
+ export interface RevokeOtherSessionsInput {
1055
+ /**
1056
+ * The Better Auth session token this device holds, if any. Proving it keeps
1057
+ * that session alive; without it every session is revoked. The calling API
1058
+ * key is always kept.
1059
+ */
1060
+ keep_session_token?: string;
1061
+ }
1062
+ export interface RevokeOtherSessionsResult {
1063
+ success: true;
1064
+ /** Better Auth sessions revoked. */
1065
+ revoked: number;
1066
+ /** Other API keys revoked (within this key's app when the key is app-bound). */
1067
+ api_keys_revoked: number;
1068
+ /** Whether keep_session_token named a session of this account, which was kept. */
1069
+ kept_session: boolean;
872
1070
  }
873
1071
  export interface DeletionStatus {
874
1072
  requested: boolean;
package/llm.md CHANGED
@@ -162,7 +162,7 @@ await r.agents.grantProjectAccess(agent.id, {
162
162
  })
163
163
  ```
164
164
 
165
- Methods: `list`, `get`, `create`, `update`, `delete`, `chat`, `chatStream`, `chatStreamText`, `conversations`, `listDiscoverable`, `leaderboard`, `grantProjectAccess`, `revokeProjectAccess`, `resetRequestCount`, `getByClaimToken`, `claim`, `orchestratorStatus`
165
+ Methods: `list`, `get`, `create`, `update`, `delete`, `chat`, `chatStream`, `chatStreamText`, `retryChatStream`, `retryChatStreamText` ("Try again": `{ conversation_id, retry_message_id }` re-answers your latest message without sending it twice), `conversations`, `listDiscoverable`, `leaderboard`, `grantProjectAccess`, `revokeProjectAccess`, `resetRequestCount`, `getByClaimToken`, `claim`, `orchestratorStatus`
166
166
 
167
167
  ### r.brain — AI assistant messaging
168
168
 
@@ -371,6 +371,24 @@ Bearer auth, with a Cookie fallback for native/server runtimes.
371
371
 
372
372
  Methods: `signUp`, `signIn`, `getSession`, `signOut`, `createApiKey`, `signInAndCreateKey`, `signUpAndCreateKey`, `verifyOtpAndCreateKey`
373
373
 
374
+ Also: `signInWithUsername` / `signInWithUsernameAndCreateKey` (handle + password, resolved on the app's network; pass `{ projectId }` as app context), `forgetPassword(input, appContext?)`, `resetPassword({ newPassword, token }, appContext?)`. Password sign-in on a two-factor account throws `AuthenticationError` with code `two_factor_required`; its `details` is `{ twoFactorToken, methods }`. Finish with `completeTwoFactorSignIn({ twoFactorToken, code, method? })` (`method: 'backup_code'` for a backup code), or `completeTwoFactorSignInAndCreateKey(input, keyInput)` after a `*AndCreateKey` sign-in. Ten wrong codes lock the account's two-factor sign-in for fifteen minutes (`ACCOUNT_TEMPORARILY_LOCKED`, `RateLimitError` with `retryAfter`).
375
+
376
+ ```typescript
377
+ // Sign-in on a two-factor account: catch two_factor_required, then send the code with the challenge.
378
+ try { await r.auth.signInWithUsername({ username, password }, { projectId }) } catch (e) {
379
+ if (e.code === 'two_factor_required') await r.auth.completeTwoFactorSignIn({ twoFactorToken: e.details.twoFactorToken, code })
380
+ }
381
+ // Two-factor: session-token calls. verify/disable rotate the session; store the returned sessionToken.
382
+ const { totpURI, backupCodes, sessionToken } = await r.auth.enableTwoFactor({ password }, session.token)
383
+ const { sessionToken: live } = await r.auth.verifyTwoFactor({ code: '123456' }, sessionToken) // turns 2FA on
384
+ // No password (profiles.me().has_password === false)? Confirm with an emailed code instead.
385
+ // Enrolling such an account answers PASSWORD_REQUIRED_FOR_TWO_FACTOR unless the server enables it.
386
+ await r.auth.requestTwoFactorEmailCode(live, { projectId }) // app context brands the email; then pass { emailCode } where you would pass { password }
387
+ await r.auth.disableTwoFactor({ emailCode: '482913' }, live)
388
+ ```
389
+
390
+ Two-factor methods: `enableTwoFactor`, `verifyTwoFactor`, `disableTwoFactor`, `regenerateTwoFactorBackupCodes`, `requestTwoFactorEmailCode`, `completeTwoFactorSignIn`, `completeTwoFactorSignInAndCreateKey`. OAuth consent screens: `exchangeSessionForCookie(token)` (browser, sets the API-origin cookie so `/api/auth/mcp/authorize` continues without a second sign-in), `getOAuthClientInfo({ clientId, redirectUri?, scope? })` (public client name, redirect hosts, `redirect_uri_registered`, granted scopes).
391
+
374
392
  **Mobile/React Native auth** (recommended — one call instead of three):
375
393
  ```typescript
376
394
  const anonSdk = new Recursiv({ anonymous: true, baseUrl });
@@ -457,9 +475,11 @@ Methods: `list`, `get`, `getBySlug`, `create`, `update`, `delete`, `members`, `a
457
475
  const { data: profile } = await r.profiles.me()
458
476
  await r.profiles.follow('user_xxx')
459
477
  const { data: results } = await r.profiles.search({ query: 'jane' })
478
+ const { data: rows } = await r.profiles.followers('user_xxx') // each row: is_following, follows_you
479
+ await r.profiles.mute('user_xxx') // server hides their posts and reposts of them from your feeds
460
480
  ```
461
481
 
462
- Methods: `list`, `me`, `get`, `getByUsername`, `search`, `update`, `follow`, `unfollow`, `followers`, `following`, `isFollowing`, `leaderboard`
482
+ Methods: `list`, `me`, `get`, `getByUsername`, `search`, `mentions` (people + agents for an @-picker, follows first), `update`, `follow`, `unfollow`, `followers`, `following`, `followingIds` (your follow set as ids, cursor-paged), `isFollowing`, `related`, `leaderboard`, `block`, `unblock`, `blocked`, `mute`, `unmute`, `muted`
463
483
 
464
484
  ---
465
485
 
@@ -474,7 +494,9 @@ const { data: group } = await r.chat.createGroup({ name: 'Team', member_ids: ['u
474
494
  const { data: msgs } = await r.chat.messages(dm.id, { limit: 50 })
475
495
  ```
476
496
 
477
- Methods: `conversations`, `communityConversation`, `conversation`, `messages`, `send`, `dm`, `createGroup`, `deleteConversation`, `reactToMessage`, `editMessage`, `deleteMessage`, `markAsRead`, `unreadCount`
497
+ Methods: `conversations`, `communityConversation`, `conversation`, `messages`, `send`, `dm`, `createGroup`, `deleteConversation`, `leave`, `acceptRequest`, `declineRequest`, `reactToMessage`, `editMessage`, `deleteMessage`, `markAsRead`, `unreadCount`
498
+
499
+ Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growing `offset`. Conversations carry `can_delete` / `can_leave`; check them before offering Delete or Leave.
478
500
 
479
501
  ### r.e2ee — End-to-end encryption transport (flag-gated)
480
502
 
@@ -518,9 +540,14 @@ const unsub = r.realtime.onMessage((msg) => {
518
540
  })
519
541
  // Cleanup listener
520
542
  unsub()
543
+
544
+ // Any server event, plus `connect` (fires on every reconnect: refetch what you missed) and `disconnect`
545
+ const off = r.realtime.on('notification', (n) => refreshBadge(n))
521
546
  ```
522
547
 
523
- Events: `onMessage`, `onMessageEdit`, `onMessageDelete`, `onTyping`, `onAgentThinking`
548
+ Events: `onMessage`, `onMessageEdit`, `onMessageDelete`, `onTyping`, `onAgentThinking`, `on(event, fn)`
549
+
550
+ Reconnects on its own (transport drops, token expiry, refused handshakes) with jittered backoff and a fresh single-use token per attempt; listeners and joined rooms carry over, so no app-side reconnect shim is needed. It stops only on `disconnect()` or a 401/403 from the token source. `connect()` is safe to call repeatedly.
524
551
 
525
552
  #### React / React Native specific hook
526
553
 
@@ -553,16 +580,19 @@ await r.posts.react(post.id, { type: 'fire' })
553
580
  const { data: results } = await r.posts.search({ query: 'shipped', community_id: 'comm_xxx' })
554
581
  ```
555
582
 
556
- Methods: `list`, `get`, `create`, `update`, `delete`, `search`, `react`, `unreact`
583
+ Methods: `list`, `get`, `create`, `update`, `delete`, `search` (`sort: 'new'` for newest first), `replies(id, { limit, cursor })` (after `replies_next_cursor` on `get`), `react`, `unreact`
557
584
 
558
585
  ### r.communities
559
586
 
560
587
  ```typescript
561
588
  const { data: c } = await r.communities.create({ name: 'Denver Devs', slug: 'denver-devs', privacy: 'public' })
562
589
  await r.communities.join(c.id)
590
+ const { data: found } = await r.communities.search('denver', { limit: 10 }) // name matches first
591
+ const { data: mine } = await r.communities.search('book', { member: true }) // only my groups
592
+ const { data: people } = await r.communities.members(c.id, { q: 'ada' }) // name or username
563
593
  ```
564
594
 
565
- Methods: `list`, `get`, `members`, `create`, `join`, `leave`
595
+ Methods: `list` (`q`, `member`, `sort`), `search`, `get`, `related`, `members` (`q`), `create`, `join`, `leave`, `requests` (`q`), moderation (`approveMember`, `banMember`, …), `createInvite`
566
596
 
567
597
  ### r.uploads — Post and chat media
568
598
 
@@ -585,6 +615,10 @@ const publicUrl = await r.uploads.uploadMedia({
585
615
  failure path; catch once at the UI boundary and tell the user. Do not repeat
586
616
  the presign/PUT sequence in application code.
587
617
 
618
+ Video: check `const { data } = await r.video.entitlement()` before offering the
619
+ picker (`data.allowed`, `data.reason` 'tier' | 'quota', `data.remainingSeconds`),
620
+ then `r.video.createUpload()`, which runs the same check.
621
+
588
622
  ### r.chat — Messaging
589
623
 
590
624
  ```typescript
@@ -594,7 +628,9 @@ const { data: group } = await r.chat.createGroup({ name: 'Team', member_ids: ['u
594
628
  const { data: msgs } = await r.chat.messages(dm.id, { limit: 50 })
595
629
  ```
596
630
 
597
- Methods: `conversations`, `communityConversation`, `conversation`, `messages`, `send`, `dm`, `createGroup`, `deleteConversation`, `reactToMessage`, `editMessage`, `deleteMessage`, `markAsRead`, `unreadCount`
631
+ Methods: `conversations`, `communityConversation`, `conversation`, `messages`, `send`, `dm`, `createGroup`, `deleteConversation`, `leave`, `acceptRequest`, `declineRequest`, `reactToMessage`, `editMessage`, `deleteMessage`, `markAsRead`, `unreadCount`
632
+
633
+ Scroll back with `messages(id, { cursor: page.meta.next_cursor })`, not a growing `offset`. Conversations carry `can_delete` / `can_leave`; check them before offering Delete or Leave.
598
634
 
599
635
  ### r.tags
600
636
 
@@ -609,10 +645,11 @@ Methods: `list`, `get`, `create`, `delete`
609
645
 
610
646
  ## Platform Resources
611
647
 
612
- - `r.settings` — User preferences, sessions, password changes, account deletion. With a project-bound key, `getPreferences()` includes app-scoped `content`; persist changes with `updateContent({ show_nsfw?, autoplay_video?, ai_enabled?, default_feed? })`.
648
+ - `r.settings` — User preferences, sessions, password changes, account deletion. `revokeOtherSessions({ keep_session_token? })` signs out every other device and keeps this key (and the proven session); `revokeAllSessions()` also revokes this key. Accounts with no password confirm `requestEmailChange({ new_email, email_code })` with a code from `requestConfirmationCode('email_change')`. With a project-bound key, `getPreferences()` includes app-scoped `content`; persist changes with `updateContent({ show_nsfw?, autoplay_video?, ai_enabled?, default_feed? })`.
613
649
  - `r.billing` — Usage tracking, checkout sessions, portal sessions, metered billing
614
- - `r.appSubscriptions` — Project-scoped consumer plans: `checkout`, `status`, `createPortalSession`
615
- - `r.notifications` — Push notification token registration/unregistration
650
+ - `r.appSubscriptions` — Project-scoped consumer plans: `checkout` (409 `subscription_exists` when the member already pays), `status`, `createPortalSession`, `previewTierChange({ tier })`, `changeTier({ tier, proration_date? })` (in-place, prorated, never a second subscription)
651
+ - `r.boosts` — Promoted posts (micro-USD): `create`, `mine`, `balance`, `pause`, `resume`, `cancel`, `recordEvent`; buy credit with `topup({ amount_cents, return_url })` then `confirmTopup(sessionId)` (credits once); admin `listPendingReview`, `review`, `grant`
652
+ - `r.notifications` — The notification stream: `list({ category: 'social' })` (rows carry `actor`, `reaction_type` and a `post_preview` with the viewer's `user_reaction`/`reposted_by_viewer_id` and counts), `unreadCount({ category })` for badges (`const { data } = await r.notifications.unreadCount({ category: 'social' })`), `markAsRead(id)`, `archive(id)` to dismiss, `markAllAsRead(category)` or `markAllAsRead({ category, organization_id })`, plus push token `registerToken`/`unregisterToken`. A push's `data.notificationId` is the row to mark read on tap. DMs are not in the stream.
616
653
  - `r.uploads` — Legacy media upload URLs (prefer `r.storage` for new projects)
617
654
  - `r.github` — GitHub integration (getRepository, getTree, getFileContent, getCommits, createFromTemplate)
618
655
  - `r.deployments` — Deployment management (prefer `r.projects.deploy()` for new projects)
@@ -620,7 +657,7 @@ Methods: `list`, `get`, `create`, `delete`
620
657
  - `r.email` — Email campaigns, batch sending, and transactional email (`r.email.send({ to, subject, html })`)
621
658
  - `r.webhooks` — Register inbound webhook endpoints: `register()`, `list()`, `delete()`. Webhook events execute handler code in project sandbox.
622
659
  - `r.jobs` — Scheduled cron jobs: `create({ name, cron, handler_code })`, `list()`, `update()`, `delete()`. Handlers execute in project sandbox on schedule.
623
- - `r.wallet` — Credits/wallet management
660
+ - `r.wallet` — Wallet, MINDS tokens (`getTokenBalance`, `getTokenLedger`; wei stays a string), USD payouts (`getPayouts`, `startPayoutOnboarding`, `payoutLoginLink`)
624
661
  - `r.freeTier` — Free tier limits and status
625
662
 
626
663
  ```typescript
@@ -629,6 +666,9 @@ if (subscription.active) {
629
666
  const { data: portal } = await r.appSubscriptions.createPortalSession({ return_url: 'https://app.example.com/billing' })
630
667
  console.log(portal.url)
631
668
  }
669
+ // Switch plan in place: preview, then change with the same proration date
670
+ const { data: quote } = await r.appSubscriptions.previewTierChange({ tier: 'pro' })
671
+ await r.appSubscriptions.changeTier({ tier: 'pro', proration_date: quote.prorationDate })
632
672
  ```
633
673
  - `r.admin` — Admin operations (user management, stats, network settings); content reports: `listReports({ status? })`, `resolveReport(id)`, `dismissReport(id)` (project-bound keys see only their app's reports)
634
674
  - `r.inviteCodes` / `r.inviteCodesAdmin` — Invite code management
@@ -638,6 +678,7 @@ if (subscription.active) {
638
678
  - Delegated social accounts: `r.protocols.nativeSocialAccounts()`, `startNativeSocialConnection({ protocol, identifier })`, `finishNativeSocialConnection({ protocol, state, code?, iss?, error? })`, `disconnectNativeSocialAccount(id)`. Example: `const { data } = await r.protocols.startNativeSocialConnection({ protocol: "bluesky", identifier: "alice.bsky.social" })`; open `data.authorizationUrl`, then complete with the same signed-in app user. Never log callback codes, send passwords, or auto-retry OAuth callbacks. Shared replies (off until schema/API deployment and live verification): `prepareNativeReply({ clientMutationId, text, parent: { protocol, nativeId, readRef? }, accountIds, media?, linkedPostAccountIds?, recoverFrom?: { batchId, deliveryIds } })`, `submitNativeReply(id, { approve: true, deliveryIds })`, `nativeReplyStatus(id)`, `nativeReplyHistory({ limit?, cursor? })`, `prepareNativeReplyRemoval(id, { deliveryIds })`, `removeNativeReply(id, { approve: true, removals })`. Review account, destination, `kind`, source, exact `wireText` and every reviewed `media` item with its alt text; connection is not publication approval. `media` takes the same `NativeMediaInput[]` as `prepareNativePost` and needs the account's `publish_media` capability. `linkedPostAccountIds` explicitly selects different-network standalone posts with a source link, never matching threaded replies or a same-network denial fallback. Preserve mutation IDs, show per-destination results and reconcile uncertain attempts without reposting. To review selected unsent or definitively rejected deliveries again, use recoverFrom with a new mutation ID, the same source/accounts and freshly reviewed text; preserve that ID on uncertain preparation. Only saved recoverable destinations qualify, current gates still apply, and replacedBy disables old sends atomically. Never restart unknown, accepted or published deliveries. The new preview still needs explicit approval. Saved delegated drafts/results: `const { data: history } = await r.protocols.nativeReplyHistory({ limit: 20 })`; use its opaque cursor for the next page and `nativeReplyStatus(item.id)` for details. This is a private read, not a write or provider refresh, and includes Nostr results marked type=nostr. Use nativeNostrReplyResult(item.id) to check that recorded event on its relay without signing or resending. Discard page state on owner/app changes. See README for examples and the callback, revocation and removal contracts.
639
679
  - `r.protocols` — Private app safety: `sourceControls` returns current `enabled` plus paged saved choices; `setSourceControl`, `onSourceControlChange` and `reportSource` share app scope. Example: `const choices = new Map(); const stop = r.protocols.onSourceControlChange(change => choices.set(JSON.stringify([change.source, change.control]), change.enabled));` then `await r.protocols.setSourceControl({ source: { protocol: "bluesky", kind: "actor", nativeId: "did:plc:aaaaaaaaaaaaaaaaaaaaaaaa" }, control: "mute", enabled: true, scope: "minds" });` and call `stop(); choices.clear()` on account change/unmount. Notifications follow an exact server confirmation, never an uncertain response. These preferences and reports apply in Minds, not on the remote network. Keyless post links: `const visitor = new Recursiv({ anonymous: true }); const { data: thread } = await visitor.protocols.getSharedConversation(projectId, { protocol: 'nostr', post: publicEventId })`; `sharedNetworks(projectId)` lists public conversation capabilities. Only configured apps; app moderation and rate limits apply, no member state or write authority. Member public reading: `publicNetworks`, `searchPublicProfiles`, `getPublicProfile`, `getPublicAuthorFeed`, `getFollowingFeed`, `getPublicConversation`, `getPublicTrends`, `getPublicTrendFeed`; private app subscriptions: `listSubscriptions`, `subscribeToProfile`, `unsubscribeFromProfile`, `onSubscriptionChange`. Example after selecting a returned source post: `const { data: thread } = await r.protocols.getPublicConversation({ protocol: post.protocol, post: post.nativeId, readRef: post.readRef })`. Render `focus`, oldest-first `ancestors` and direct `replies`; `public_provider_window` never promises every reply, and `limited` marks known omissions. Requires both `users:read` and `posts:read` on the current human app key. Protocol discovery/settings: `const { data: adapters } = await r.protocols.list()` returns bounded `{ id, name, icon, color, maturity, capabilities, limitations }` metadata; `supported` means implemented, not configured, healthy, authorized, or enabled. Methods: `list`, `status`, `listCandidates`, `getCandidate`, `search`, `getSettings`, `refresh`, `updateSettings`.
640
680
  - Native Nostr: `const { data: preview } = await r.protocols.prepareNativeNostr(identityId, { action: "publish", content: draft })`; review and sign the exact public event, then `submitNativeNostr(identityId, { intent: preview.intent, event, approve: true })`. Preparation/submission never auto-retry. After uncertainty, use `getNativeNostrPost(identityId, preview.postId ?? preview.eventId)` before a user-selected retry of the identical intent/event: deletion checks the original note's `postId`, publication checks `eventId`. Check `nativeNostrAccount()` enablement first; keys stay with the signer. For replies, after `nativeNostrAccount()` advertises `reply_text_note`, use `prepareNativeNostr(identityId, { action: "reply", content: draft, parentId: post.nativeId })`. Review returned parent/root/participants/relay and exact event, sign with the connected client signer, then `submitNativeNostr(identityId, { intent, event, approve: true })`. Requires users:read, posts:read and posts:write. Kind1 public notes only; reply enablement and shared UI/live proof remain pending. Do not inherit arbitrary notification tags or relay hints. Signing mutations never auto-retry; check `getNativeNostrPost(identityId, eventId)` after uncertainty before a user-selected identical-event retry.
681
+ - Hosted Bluesky account on the app's own PDS: `const { data } = await r.protocols.createHostedIdentity({ protocol: 'atproto', handle: 'alice', email })`. Only the label is sent; `data.handle` carries the domain and `data.password` is returned this once, never stored. Never auto-retried. Either the account is finished or it is removed again. 409 `handle_unavailable` = choose another label; 503 `handle_dns_unavailable`/`handle_unresolvable`/`handle_unverified` = operator setup, retry later unchanged.
641
682
  - Anonymous discovery: `const visitor = new Recursiv({ anonymous: true }); const { data } = await visitor.protocols.getSharedTrends(projectId, { protocol: 'nostr', limit: 20, discovery: true })`. Use `sharedNetworks(projectId)` capabilities; open a returned topic with `getSharedTrendFeed(projectId, { protocol: 'bluesky', topic: trend.nativeId, cursor })`. Configured apps only; no account authority or writes. App moderation and per-IP/app rate limits apply. Categories and Jev layouts reuse cached judgments without initiating paid inference; handle partial categories and finite provider windows.
642
683
  - Public trends: require `public_trends` from `publicNetworks()`, then `getPublicTrends({ protocol, limit?, cursor? })`; render `kind: topics` as source topics and `kind: posts` as source posts. With `trend_feed`, open only a returned current topic using `getPublicTrendFeed({ protocol, topic: trend.nativeId, limit?, cursor? })`. Show provider, observedAt and provider_ranked_window coverage; null rankingWindow is unknown, not a daily/global ranking. Preserve source order, canonical IDs and readRef, and opaque cursors. Topic catalogs do not paginate; Mastodon trends stop at 200 source positions. Both methods require users:read plus posts:read on the current human app key; Nostr trends are unsupported. Propagate errors; trend_unavailable means refresh the topic list. No ingestion or native writes.
643
684
  - `listCandidates({ protocol?, limit?, cursor? })` and `getCandidate(id)` are read-only quarantine review, not published-post search. Both require `admin` scope, a project-bound key, a live admin role, and existing project-admin access. The key supplies app/network scope; neither method accepts another project or network. They never invoke refresh, change settings, or publish.
@@ -790,12 +831,15 @@ await r.agents.create({ name: 'Bot', model: 'anthropic/claude-sonnet-4' }) // fa
790
831
  await r.agents.create({ name: 'Bot', username: 'bot_1', model: 'anthropic/claude-sonnet-4',
791
832
  system_prompt: '...', organization_id: orgId })
792
833
 
793
- // ❌ WRONG: not handling private communities
794
- await r.communities.get('comm_xxx') // 404 if private and not a member
834
+ // ❌ WRONG: assuming get() always returns the full group
835
+ const { data } = await r.communities.get('comm_xxx')
836
+ data.created_by.name // undefined on a private group you are not in
795
837
 
796
- // ✅ RIGHT
797
- try { await r.communities.get('comm_xxx') }
798
- catch (e) { if (e.status === 404) { /* private or doesn't exist */ } }
838
+ // ✅ RIGHT: a private group you're not in is a card (limited: true); hidden or missing is 404
839
+ try {
840
+ const { data } = await r.communities.get('comm_xxx')
841
+ if (data.limited) { /* show name + "Request to join" / membership_status 'pending' */ }
842
+ } catch (e) { if (e.status === 404) { /* hidden or doesn't exist */ } }
799
843
  ```
800
844
 
801
845
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@recursiv/sdk",
3
- "version": "0.7.50",
3
+ "version": "0.7.51",
4
4
  "description": "TypeScript SDK for the Recursiv API",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "type": "module",