Rising Tide Developers
SDKCore

rtClient.users

SDK methods for reading users, updating profiles, querying activity, and powering the aggregate profile surfaces.

The users service covers identity (read / create / update), per-user activity aggregates that power rt-profile, and the wallet-connect side effect that links users to apps. Substrate-canonical methods take stxAddress: string (a Stacks address); legacy methods take userId: number.

Identity

rtClient.users.userExists(stxAddress)

Boolean - does a user record exist for this address?

const exists = await rtClient.users.userExists("SP...");

rtClient.users.getUserByAddress(stxAddress)

Single user with relations (UserWithRelations).

const { data: user } = await rtClient.users.getUserByAddress("SP...");

rtClient.users.getUserById(id)

Same as above by numeric id.


rtClient.users.batchLookupUsers(addresses)

Bulk lookup. Returns { results: { stxAddress, user }[], found, notFound, total }.


rtClient.users.searchUsers(query, params?)

Search the user directory.


rtClient.users.createUser(userData)

await rtClient.users.createUser({
  displayName: "Alice",
  stxAddress: "SP...",
  email: "alice@example.com",
  inviteCode: "ABCD-EFGH-1234",  // required if app has requireInviteForSignup
});

When the app has requireInviteForSignup, the invite code is consumed inside the create transaction - if the invite is invalid the user is not created (single code path, zero race window).


rtClient.users.updateUser(id, patch)

Update display name, bio, avatar, social handles, payment prefs.

await rtClient.users.updateUser(userId, {
  displayName: "Alice in Wonderland",
  bio: "writer and collector",
  acceptsTips: true,
});

Wallet connect side effect

Idempotent, throttled. Call on every wallet connect to ensure the calling app is linked to the authenticated wallet. Returns { exists, linked } - safe to ignore the response.

await rtClient.users.touchAppLink();

Powers cross-app activity aggregation in rt-profile - a user who only browses still shows up as "active in this app" so their ProfileRail reflects reality.

Activity stats

There is no separate activity-stats method. The per-user aggregate is the totals block of getUserProfile, which is also what the small badges next to a creator's name read.

Cross-domain instances

rtClient.users.getUserInstances(stxAddress, params?)

Paginated ProductInstance rows owned by a wallet across PAGE / EVENT / PREDICTION / GAME in one call. Each row is joined with parent SmartProduct + ProductType for unified rendering.

const res = await rtClient.users.getUserInstances(stxAddress, {
  domain: "PAGE",
  status: "CONFIRMED",
  parentProductId: 42,
  sortBy: "createdAt",
  sortOrder: "desc",
  include: "page",  // embed the full SmartProductPage on each row
  page: 1,
  limit: 50,
});
ParamTypeDescription
domain'PAGE' | 'EVENT' | 'PREDICTION' | 'GAME'Filter by domain
status'PENDING' | 'SUBMITTED' | 'CONFIRMED' | 'FAILED' | 'DROPPED'On-chain status
parentProductIdnumberFilter to one parent SmartProduct
sortBy'createdAt' | 'instanceNumber' | 'feePaid'Sort key
sortOrder'asc' | 'desc'Direction
page / limitnumberPagination
include'page'Embed parent SmartProductPage on each row

Profile aggregate

rtClient.users.getUserProfile(stxAddress)

Single-call profile aggregate combining user record, links, app participation, and payment totals. Designed for the rt-profile overview - one request instead of five.

const { data: profile } = await rtClient.users.getUserProfile(stxAddress);

Creator hub aggregates (0.13.0)

rtClient.users.getUserCollectors(stxAddress, params?)

Audience query: one row per distinct ownerAddress on the creator's ProductInstance rows, joined with User when an account exists. Powers the Creator Hub audience panel.

rtClient.users.getUserSupporting(stxAddress, params?)

Inverse of collectors: creators a user is supporting, with edition + product counts and first/last support timestamps. Powers the rt-profile /earnings Support tab.

rtClient.users.getUserFeed(stxAddress, params?)

Personalized feed: recent published products from creators the user has already collected from. Cursor pagination.

const { data: feed } = await rtClient.users.getUserFeed(stxAddress, {
  limit: 20,
  cursor: undefined,
});

Achievements

rtClient.users.getAchievements(stxAddress, params?)

The shelf: what an address has earned, and for the person it belongs to, the one thing to do next. Returns an AchievementShelf - awards (newest first, each with its line already resolved, the appId / appName it happened in, and an artUrl) and next.

artUrl is absolute, because the shelf renders in several apps on several origins. It falls back to the icon of the app the rung is walked in, and is null when there is neither - a surface renders no image rather than a broken one.

next is the person's own view only. It is included when the request carries the bearer token for that address - a signed-in client sends it on every call - and it is exactly one rung: { slug, line, n, of, appId, appName, appUrl, graduated }. A stranger, or a call with no token, gets the awards and next: null. So a null next on your own address means the token was not on the wire, not a finished ladder; finished is next.graduated === true, with its own closing line and nothing to open. Nothing earned is an empty awards, never an error.

Pass appId for the app your surface belongs to. A door that has just opened reads as news everywhere except in the app behind it, where the instruction for the next rung is the useful sentence.

const { data: shelf } = await rtClient.users.getAchievements(stxAddress, { appId: APP_ID });
if (shelf?.next && !shelf.next.graduated) {
  // shelf.next.line is the sentence; when shelf.next.appId is not yours, shelf.next.appUrl is the action
}

Most surfaces want useNextRung from @rising-tide/ui/react instead, which reads this for the signed-in address once the token is ready.

rtClient.users.getLadder()

The whole climb, the same for everybody. Returns a Ladder - rungs (the onramp, in the order it is climbed) and returns (earned by coming back). No address and no bearer token, so a signed-out visitor can read it.

Each LadderRung is { slug, name, family, requirement, need, pays, appId, appName, appUrl, renewsEvery, artUrl, precursor }. requirement and pays are written for somebody who has not reached the rung; precursor is the slug of the rung that must be held first, null on the first rung and on every return rung.

const { data: ladder } = await rtClient.users.getLadder();
const held = new Set(shelf?.awards.map((a) => a.slug) ?? []);
for (const rung of ladder?.rungs ?? []) {
  const locked = rung.precursor !== null && !held.has(rung.precursor);
  // locked: dim it, and say "after <the precursor's name>"
}

Notifications

rtClient.users.getNotifications(stxAddress, params?)

What happened to this address while they were not looking - the itemised feed behind an inbox and behind Web Push. Returns ApiResponse<NotificationFeedPage>: items, nextCursor, unreadCount, unreadCapped (true when the count is a floor) and readAt. params is { appId?, limit?, cursor? }; omit appId for every app the address has touched, and pass it to scope an app's inbox to its own kind of news.

const { data: feed } = await rtClient.users.getNotifications(stxAddress, { appId: APP_ID, limit: 20 });

Three properties the server already guarantees, so a client does not implement them:

  • Your own acts are absent. Filtered at the transaction level: leaving a note on your own product does not notify you.
  • One act is one item. A collect writes a mint plus its payment legs; they arrive collapsed, so subject and amountSbtc describe the same moment.
  • amountSbtc is what you received, in sats as a decimal string, not what the buyer paid.

Each NotificationItem carries a kind - product.collected, payment.received, product.delivered, achievement.unlocked, invite.claimed, invite.granted, match.resolved or asset.reviewed - and the one detail block that kind uses (subject, achievement, invite, granted, match, review; the others are null), plus buyer, occurredAt and unread. product.delivered is the instance an auto-mint capability hands out at deploy: nobody collected anything, so do not word it as a collect. A win is never reported to the winner, so match.resolved is only a loss or a draw. Render every kind, or show a neutral line for one you do not know.

rtClient.users.markNotificationsRead(stxAddress, readAt?)

Moves the read watermark, so a phone and a laptop agree on what is unread. Call it when a person opens their notifications, never when the feed is fetched. Pass readAt as the occurredAt of the newest item actually shown, so anything that arrives between the render and the tap stays unread; omit it to mean "now". Idempotent and monotonic - the watermark never moves backwards. Unlike the feed, it refuses an address with no account.

App rail

rtClient.users.getAppRail(stxAddress, options?)

The cross-app rail for an address: each app with a state of OPEN, UNLOCKED or LOCKED, derived server-side from the app's access policy and the viewer's grant. options.includeHidden adds apps the user has hidden. A UserApp row is an activity footprint and never confers access. useUserApps() from @rising-tide/ui/shell wraps this.

Storage

getStorage() reads the caller's storage posture, enableStorage(address) records the storage key the app created in the user's session, setSaveAsDefault(destination) sets where editions are written automatically, and createStorageCheckout(input) starts a card payment that adds room. All act on the caller and need a verified token. See Storage and Storage - upload and archival.

Per-app preferences

rtClient.users.updateAppPreferences(stxAddress, appId, prefs)

await rtClient.users.updateAppPreferences(stxAddress, appId, {
  displayInRail: false,  // hide this app from the user's ProfileRail
});

This is the only typed PATCH surface for user-app state - direct getHttpClient().patch() from consumers is no longer supported.

Linked accounts and roles

The service exposes typed CRUD for child collections per user: getUserLinks / createUserLink / updateUserLink / deleteUserLink, getUserApps (read-only activity - granting access is now a first-class AppAccessGrant, see Entitlements), getUserRoles / assignUserRole / removeUserRole, getUserTestAccounts / createUserTestAccount / updateUserTestAccount / deleteUserTestAccount, getUserInvites. Most consumers do not need these directly - they back operator surfaces.

Admin methods

Granting and revoking roles, listing all users with filter, and similar operator actions live on rtAdmin.users in @rising-tide/sdk/admin. See Admin.