Skip to content

Setup & identity

Looking things up on XChat needs only a logged-in session. Sending anything needs an XChat identity: two P-256 keypairs whose public halves are published to X and whose private halves you keep.

You can create a key once, persist it, and load it on later runs.

js
import Emusks from "emusks";
const client = new Emusks();
await client.login("your_auth_token");

const identity = await client.xchat.createIdentity({ pin: "2468" }); // first run

// save `identity` somewhere safe, then on later runs:
await client.xchat.loadIdentity(identity);

client.xchat.createIdentity(opts?)

Generate a fresh chat identity, publish the public half, back it up under your PIN, and load it into the client. Returns a serializable identity object: persist it (it contains your private keys) and reuse it with loadIdentity.

By default this is PIN-backed with Juicebox, just like what the normal XChat uses, so the identity can be recovered with the PIN. Use the same passcode format the app uses, a 4-digit code, so the identity is also unlockable from the official X UI (the realms accept arbitrary bytes, but the app's passcode field is 4 digits).

OptionTypeDescription
pinstringYour passcode (4 digits, like the X app). Required unless selfCustody is set. Backs the keys up to the realms.
selfCustodybooleanSkip the PIN backup: you hold the only copy of the keys (no PIN recovery).
registrationMethodstringOverride the published method (CustomPin for PIN-backed, SelfCustody, ManagedPin).
js
// PIN-backed (default, like the app)
const identity = await client.xchat.createIdentity({ pin: "2468" });
await fs.writeFile("xchat-identity.json", JSON.stringify(identity)); // also holds your private keys

// or self-custody (no PIN backup)
const local = await client.xchat.createIdentity({ selfCustody: true });

TIP

PIN-backed setup uses a bundled WebAssembly build of the Juicebox SDK; it loads only when you actually create or recover a PIN-backed identity. If the backup fails, createIdentity rolls back the published key so you never end up with an un-backed-up identity. Either way, the returned object also holds the private keys, so persist it.

WARNING

With selfCustody: true there is no cross-device PIN recovery: lose the identity object and you lose access to that identity's chats.

client.xchat.loadIdentity(identity)

Load a previously created identity so you can send. Pass the object from createIdentity (or one assembled from saved key material). Prefer using this over xchat.recover when you can, since it's much faster and doesn't require the entire very complex recovery process.

FieldTypeDescription
versionstringYour published key version
publicKeyB64stringYour identity public key (SPKI base64)
identityPrivateJwk / signingPrivateJwkobjectYour private keys as JWK
userIdstringOptional; defaults to the logged-in user
js
const identity = JSON.parse(await fs.readFile("xchat-identity.json", "utf8"));
await client.xchat.loadIdentity(identity);

Once loaded, head to Sending messages.

client.xchat.recover(pin)

Recover your identity from X's PIN-protected realms using only your PIN and this logged-in session, no saved key file. This is the "new device" flow: it fetches your published key, pulls the backed-up private keys from the realms with your PIN, reconstructs the keypairs, and loads them. After it returns you can read and send on all your existing conversations.

ParamTypeDescription
pinstringThe PIN you used with createIdentity
js
const client = new Emusks();
await client.login("your_auth_token");

await client.xchat.recover("2468"); // identity restored from the realms, nothing stored locally

for (const convo of await client.xchat.conversations()) {
  const { messages } = await client.xchat.read(convo.conversationId.split(":").find((id) => id !== me.id));
  // ... all your chats, decrypted
}

Only works for a PIN-backed identity (the default). If you used selfCustody: true, there is nothing in the realms to recover; loadIdentity your saved object instead.

not affiliated with X Corp.