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
rtClient.users.touchAppLink()
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,
});| Param | Type | Description |
|---|---|---|
domain | 'PAGE' | 'EVENT' | 'PREDICTION' | 'GAME' | Filter by domain |
status | 'PENDING' | 'SUBMITTED' | 'CONFIRMED' | 'FAILED' | 'DROPPED' | On-chain status |
parentProductId | number | Filter to one parent SmartProduct |
sortBy | 'createdAt' | 'instanceNumber' | 'feePaid' | Sort key |
sortOrder | 'asc' | 'desc' | Direction |
page / limit | number | Pagination |
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
subjectandamountSbtcdescribe the same moment. amountSbtcis 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.