Rising Tide Developers
Concepts

Invites

Invites are first-class protocol objects that grant access, optionally assign a role, carry app grants plus starter STX, and can be locked to a verified identity.

Invites are first-class protocol objects. An invite is a unique code that grants access to the protocol and, when claimed, optionally assigns a role, grants access to specific apps, and funds the new wallet with starter STX. It can be handed to anyone who opens the link (bearer) or locked to a specific verified identity. A funded invite is the onboarding path - the invitee lands with a role, app access, and enough gas to act, all in one claim.

State machine

start → DRAFT (on create)
DRAFT
  • SENTsend - by the creator or the holder
  • EXPIREDan assigned draft past expiresAt
SENT
  • CLAIMEDclaim - atomic with user create
  • REVOKEDrevoke
  • EXPIREDcron + expiresAt
CLAIMEDterminal
REVOKEDterminal
EXPIREDterminal

Every transition is atomic. There is no unclaim - claiming is final, and the grant it produced records the invite it came from. Only a SENT invite can be revoked; a DRAFT that was never sent is discarded with a hard delete.

Schema

interface Invite {
  id: number;
  code: string;                     // 60-bit Crockford Base32, CSPRNG-generated
  status: InviteStatus;
  creatorId: number;                // who minted it - the accountability row
  holderUserId: number | null;      // who may SEND it; null until assigned
  holder?: { id: number; displayName: string; stxAddress: string } | null;
  addressing: InviteAddressing;      // where it may be SENT; set at mint, never patched
  claimBinding: InviteClaimBinding;  // who may claim (verified-identity lock)
  bindingValue: string | null;       // normalized email / X handle / c32 address; null for BEARER + USER
  recipientUserId: number | null;    // the USER binding's target (an existing account)
  roleId: number | null;            // role granted on claim
  appId: number | null;             // attribution/scope only - NOT an access grant
  grantAppIds: number[];            // apps this invite grants access to on claim
  fundingStxMicro: string | null;   // starter STX in microSTX (string); null = none
  claimedAt: string | null;
  expiresAt: string | null;
  createdAt: string;
}

type InviteStatus = "DRAFT" | "SENT" | "CLAIMED" | "REVOKED" | "EXPIRED";
type InviteClaimBinding = "BEARER" | "EMAIL" | "X_HANDLE" | "STX_ADDRESS" | "USER";
type InviteAddressing = "ANY" | "WAITLIST_ONLY";

Code security

Invite codes are 60 bits of CSPRNG entropy encoded as Crockford Base32. They are compared in constant time, and the preview and claim endpoints are rate-limited more tightly than the rest of the API.

Claim bindings

By default an invite is BEARER - claimable by whoever opens the link first. It can instead be locked to a specific verified identity, so a forwarded or intercepted link only works for the intended person:

BindingChecked againstIntended claimer
BEARERnothingthe first authenticated claimer (default)
EMAILthe Privy-verified email on the auth tokena specific email / Google login
X_HANDLEthe Privy-verified X (twitter) usernamea specific X login
STX_ADDRESSthe authenticated wallet addressa specific wallet (works for BYOW)
USERrecipientUserIda specific existing account

The load-bearing rule: a binding is always checked against a verified identity attribute from the auth token, never a self-reported profile field. claimBinding names the type and bindingValue holds the target (a normalized email, X handle, or c32 address); USER uses the recipientUserId relation.

A mismatch fails with a single generic rejection, and neither preview nor claim ever reveals the target value - so a code can't be used to enumerate who an invite was for. Preview returns only boundTo (the binding type) so a claim screen can prompt the right sign-in, and rtClient.invites.precheckClaim(code) confirms eligibility right after sign-in - before a new user sets up a profile - rather than only when the atomic signup rolls back.

Where an invite may go

Three questions, three fields. creatorId is who minted it, holderUserId is who may send it, claimBinding is who may claim it - and addressing is where the send may go.

addressingSet onSending it
ANYevery operator mint, and the Graduated invite the ladder pays oncesend to anyone, or sendToWaitlist
WAITLIST_ONLYthe return-5 renewal (one per five active days) and the weekly dripsendToWaitlist only

send on a WAITLIST_ONLY invite is refused with INVALID_ADDRESSING (400) whoever sends it and whether or not a recipient is typed. The rule in one line: one-time rewards buy loyalty, so the earner aims them; recurring supply buys growth, so it drains the pool. Existing invites are ANY.

Role on claim

If roleId is set, that role is granted to the claiming user at claim time. Role hierarchy is enforced - you cannot grant a role higher than your own.

Funded invites

An invite can carry two extra payloads that provision the moment it is claimed:

  • grantAppIds - app IDs the claim grants real access to. Each becomes a UserApp row (idempotent upsert), so the granted apps show up on the newcomer's app rail immediately. This is distinct from the scalar appId, which is attribution/scope only and grants nothing.
  • fundingStxMicro - starter STX (in microSTX) sent to the claimer's wallet from the protocol float, so a brand-new user has gas to act.

Ordering matters for durability:

claim tx
status -> CLAIMED, role + UserApp grants, atomic
commit
grants are now durable
fund wallet
post-commit STX, best-effort

The role grant and every app grant land atomically with the status flip - all-or-nothing. Wallet funding runs after that commits, and is best-effort: a hiccup there never rolls back a claim. The invitee keeps their role and app access even if the top-up needs a retry.

For the creator-facing view of this path, see Funded invites and the create-to-earn economy.

Mint quotas by role

Mint permission is derived from the creator's highest role tier, not a dedicated entitlement. A client can read its own authority up front with rtClient.invites.getMintAuthority() rather than showing a form that only 403s on submit.

RoleDaily mint quota
Member0
Open Beta0
Early Adopter0
AdminUnlimited

Inviting has not gone away for the other tiers - it moved. An Admin mints an invite and assigns it to a holder (rtClient.invites.assign); the holder sends it and binds it to a person; the invitee claims it. The mint quota gates creation only, so a holder sends what they were handed.

API endpoints

MethodPathDescription
POST/api/invitesCreate an invite
GET/api/invitesList your invites
POST/api/invites/previewPreview by code (body { code }) - creator, role, granted apps, boundTo; app key, no user token
POST/api/invites/precheckRead-only claim eligibility for the signed-in caller (works before an account exists)
POST/api/invites/claimClaim as the authenticated user
GET/api/invites/mint-authorityThe caller's own mint authority (role-tier derived)
GET / PATCH / DELETE/api/invites/:idRead one, patch a DRAFT, delete a DRAFT
POST/api/invites/:id/assignHand a DRAFT to a holder (Admin)
POST/api/invites/:id/revokeRevoke a SENT invite
POST/api/invites/:id/sendSend (DRAFT -> SENT)
POST/api/invites/:id/send-to-waitlistSpend a held, unaddressed DRAFT on someone from the waitlist pool

See InviteStatus for the full transition table.