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
- SENTsend - by the creator or the holder
- EXPIREDan assigned draft past expiresAt
- CLAIMEDclaim - atomic with user create
- REVOKEDrevoke
- EXPIREDcron + expiresAt
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:
| Binding | Checked against | Intended claimer |
|---|---|---|
BEARER | nothing | the first authenticated claimer (default) |
EMAIL | the Privy-verified email on the auth token | a specific email / Google login |
X_HANDLE | the Privy-verified X (twitter) username | a specific X login |
STX_ADDRESS | the authenticated wallet address | a specific wallet (works for BYOW) |
USER | recipientUserId | a 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.
addressing | Set on | Sending it |
|---|---|---|
ANY | every operator mint, and the Graduated invite the ladder pays once | send to anyone, or sendToWaitlist |
WAITLIST_ONLY | the return-5 renewal (one per five active days) and the weekly drip | sendToWaitlist 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 aUserApprow (idempotent upsert), so the granted apps show up on the newcomer's app rail immediately. This is distinct from the scalarappId, 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:
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.
| Role | Daily mint quota |
|---|---|
| Member | 0 |
| Open Beta | 0 |
| Early Adopter | 0 |
| Admin | Unlimited |
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
| Method | Path | Description |
|---|---|---|
POST | /api/invites | Create an invite |
GET | /api/invites | List your invites |
POST | /api/invites/preview | Preview by code (body { code }) - creator, role, granted apps, boundTo; app key, no user token |
POST | /api/invites/precheck | Read-only claim eligibility for the signed-in caller (works before an account exists) |
POST | /api/invites/claim | Claim as the authenticated user |
GET | /api/invites/mint-authority | The caller's own mint authority (role-tier derived) |
GET / PATCH / DELETE | /api/invites/:id | Read one, patch a DRAFT, delete a DRAFT |
POST | /api/invites/:id/assign | Hand a DRAFT to a holder (Admin) |
POST | /api/invites/:id/revoke | Revoke a SENT invite |
POST | /api/invites/:id/send | Send (DRAFT -> SENT) |
POST | /api/invites/:id/send-to-waitlist | Spend a held, unaddressed DRAFT on someone from the waitlist pool |
See InviteStatus for the full transition table.
How State Syncs
Writes land on chain first and reach the API on confirmation. What your app can observe at each step, and the guarantees you can build on.
Achievements
The progression ladder - awards derived from the event stream that open apps and mint invites, and the two read surfaces an app builds on.