rtClient.waitlist
SDK methods for taking requests to be let in, reading one back, and the pool members spend invites on.
The public half of the waitlist. signup, status and openSpots work with only an app API key - no signed-in user,
which is the point: the person asking has no account, and creating one for them before they are let
in is the dead end this surface exists to remove.
A signup is a request, not an invite. Nothing here mints, funds, or grants anything. An operator converts a request into an invite by hand, through the ordinary mint-and-send path.
Two more reads serve other callers: openSpots is public, and pool is for signed-in members. The
operator queue - list, approve, decline, publish, withdraw, annotate, requeue and
remove - is admin-only and is not something a client app calls. See the
Waitlist API reference.
Methods
rtClient.waitlist.openSpots()
How many applicants are let in on the spot right now. Public, like signup. Returns ApiResponse<WaitlistOpenSpots>; open is null when nobody is. While spots are open, signup returns a request that is already INVITED, so its status carries a claimUrl at once. Say it as "right now", never as a promise - a slot can go before someone submits.
rtClient.waitlist.pool(params?)
The applicants who agreed to be read, for the people who can act on it - the pool a holder spends an invite on with invites.sendToWaitlist. Needs a signed-in member (401 for anyone else). The order is shuffled per viewer and stable across their pages; it is not a ranking and there is no total.
rtClient.waitlist.signup(input)
Record a request. Returns ApiResponse<WaitlistSignupResult> - { statusToken }, and nothing else.
const { data } = await rtClient.waitlist.signup({
channel: "EMAIL", // or "X_HANDLE"
contact: "ana@gmail.com", // as typed; normalized server-side
pitch: "...", // optional - at most 800 characters after trimming
currentWork: "...", // optional - at most 500 characters after trimming
interests: ["WRITING"], // at least one
experience: "NEW_TO_CRYPTO",
referrerAppId: 10, // optional - where they came from; never ask for it
});
console.log(data.statusToken);A duplicate returns the same 200, the same body and the same token as a new signup. Do not
branch on new-versus-duplicate - you cannot see it, deliberately, because a different response
would let anyone test whether a given person had applied. A contact that does not parse still
returns a normal 400 with INVALID_CONTACT: that is the submitter's own typo, not
information about a third party.
The contact is normalized with the same rules the invite bindings use, so Ana+RT@Gmail.com and
ana@gmail.com are one row, as are @Handle, handle and HANDLE.
Rate-limited per IP and fail-closed, so treat RATE_LIMITED as a real state your form must render.
rtClient.waitlist.status(token)
Read one request back by its statusToken. Returns ApiResponse<WaitlistStatus>.
const { data } = await rtClient.waitlist.status(token);
if (data.status === "INVITED") {
// data.claimUrl, data.contactRaw, data.expiresAt
}Nothing is ever sent to an applicant - there is no mail channel and no way to DM an X handle - so the token is the whole thread back to the request. Store it and hand it to the person as a URL; a browser-local copy is a useful second chance, never a replacement for the link.
While PENDING the payload is just status and createdAt. Once INVITED it also carries
claimUrl, expiresAt, invitedAt and contactRaw.
contactRaw is the address they typed, and this is the only response allowed to name it.
InvitePreview.boundTo carries the binding TYPE only - no recipient-enumeration oracle - so a claim
page can say "sign in with Google" but never which Google account, which is a loop for
anyone with two of them. This payload is scoped to the person who filled in the form, so echoing
their own submission back leaks nothing and removes the cliff.
An unrecognized token resolves to { status: "UNKNOWN" } with a 200, never a 404. The endpoint
never confirms whether a token was real, so render UNKNOWN the same way you render a request that
has not been accepted.