Skip to content

With the TypeScript SDK

The TypeScript SDK, @stxapp/stx-typescript, runs the whole OAuth flow for you on Node.js 18+ and Bun: it creates the PKCE verifier and state, exchanges the code, refreshes and rotates tokens, and revokes them. How the flow works, what each scope allows and how to handle tokens safely are covered in the OAuth section; this page shows the SDK calls.

npm install @stxapp/stx-typescript

Everything on this page is imported from @stxapp/stx-typescript/oauth. Create one OAuthClient per registered app, on your server:

import { MemoryPendingAuthorizationStore, MemoryTokenStore, OAuthClient } from "@stxapp/stx-typescript/oauth";
const oauth = new OAuthClient({
// baseUrl defaults to STX_HOST: the demo host from your invite.
clientId: process.env.STX_CLIENT_ID!,
clientSecret: process.env.STX_CLIENT_SECRET, // keep it on the server
redirectUri: "https://yourapp.example/stx/callback", // exactly as registered
scope: "profile.read balance.read portfolio.read orders.read orders.write",
});
// Sign-ins in progress, and each member's tokens.
const pending = new MemoryPendingAuthorizationStore();
const tokens = new MemoryTokenStore();

The in-memory stores suit one process. In production, implement TokenStore (get, set, delete) and PendingAuthorizationStore (put, take) over your database, and encrypt tokens at rest. If several processes share one token store, also implement withLock, so only one of them refreshes a member’s token at a time.

Scope names are also exported as constants: MemberScopes.ORDERS_WRITE, AppScopes.MARKET_DATA and so on. What each one allows is listed in Scopes.

Two routes on your server: one sends the member to STX, the other receives them back. Here with Express:

// 1. Send the member to STX to sign in and approve your app.
app.get("/stx/link", async (req, res) => {
const { url } = await oauth.beginAuthorization(pending, { data: { userId: req.user.id } });
res.redirect(url);
});
// 2. STX sends them back with a code. Exchange it and store the tokens under your own user id.
app.get("/stx/callback", async (req, res) => {
const done = await oauth.completeAuthorization(pending, req.url, {
store: tokens,
memberKey: (data) => String(data?.userId),
});
res.send(`Your STX account is linked (${done.scopes.join(", ")}).`);
});

beginAuthorization() creates the PKCE verifier and state and keeps them in pending; completeAuthorization() checks the state, exchanges the code and saves the tokens. done.scopes is what the member granted, which can be less than you asked for. The steps it runs are described in Authorization code flow.

If one callback URL serves several apps, split the second step: readCallback(pending, req.url) returns the code and the pending entry (with your data), and redeemAuthorization() on the right app’s OAuthClient finishes it.

memberClient() returns an ordinary STX client that acts for one member. Every method works as it does with an API key, within the scopes the member granted:

const member = oauth.memberClient(tokens, userId);
const balance = await member.balance();
console.log(balance.available_balance);
const open = await member.orders({ status: ["open"] });
console.log(`${open.length} open orders`);
await member.placeOrder(marketId, "buy", "limit", { price: "0.40", quantity: "1" });

The client refreshes the access token before it expires and after a 401, and writes the new tokens to your store (STX rotates refresh tokens, so the old one stops working; see Refreshing). Pass { session: { onRefresh, onGrantRevoked } } to hear about either.

The member’s channels work the same way:

const ws = await member
.websocket({
// Called if access ends while the socket is open (revoked, or the token expired).
onAuthError: (err) => console.log("link again:", err.message),
})
.connect();
await ws.orders({ onMessage: (msg) => console.log(msg.event, msg.payload) });

With a client secret, your app can read the market catalogue and market data channels as itself, with no member involved (an app token):

const api = oauth.appClient("market_data");
const page = await api.markets({ status: "open", limit: 5 });
for (const market of page.items) console.log(market.symbol, market.bids?.[0]?.price ?? "-");

The app token is cached and renewed for you.

const revoked = await oauth.unlink(tokens, userId);

unlink() revokes the member’s grant at STX and deletes their tokens from your store. It resolves false if STX could not be reached; the local tokens are deleted either way.

Every OAuth error extends STXAuthenticationException or STXForbiddenException, and they are exported from @stxapp/stx-typescript/oauth:

Exception When What to do
STXGrantRevokedException The member revoked your app, or the grant ended. Their tokens are already deleted from your store. Show “Link your STX account” again.
STXInsufficientScopeException The grant lacks a scope the call needs; err.requiredScopes names it. Link again, asking for that scope.
STXOAuthException The callback or token endpoint returned an error; err.error holds the code, such as access_denied when the member declines. Depends on the code; see Discovery and errors.
STXInvalidTokenException The access token was refused even after a refresh. Link again.
import { STXGrantRevokedException, STXInsufficientScopeException } from "@stxapp/stx-typescript/oauth";
try {
await member.placeOrder(marketId, "buy", "limit", { price: "0.40", quantity: "1" });
} catch (err) {
if (err instanceof STXGrantRevokedException) {
res.redirect("/stx/link");
} else if (err instanceof STXInsufficientScopeException) {
console.log("needs", err.requiredScopes);
} else {
throw err;
}
}

The package ships full TypeScript declarations for @stxapp/stx-typescript/oauth, so your editor shows every option and its documentation. The main pieces:

Export What it is
OAuthClient One registered app. beginAuthorization(), completeAuthorization() and redeemAuthorization() link a member; memberClient() and appClient() return an STX client; unlink() and revoke() disconnect; refreshToken(), clientCredentials() and introspect() call the token endpoints directly. OAuthClient.fromMetadata() builds one from the server metadata.
TokenStore, MemoryTokenStore Where each member’s token pair is kept: get, set, delete, and optionally withLock.
PendingAuthorizationStore, MemoryPendingAuthorizationStore Where a sign-in in progress keeps its PKCE verifier and state: put and take.
readCallback() Reads the code and the pending entry from a callback URL, for a callback shared by several apps.
MemberScopes, AppScopes The scope names as constants.
discoverMetadata(), registerClient() Fetch the server metadata, and register a client where dynamic registration is enabled.
STXOAuthException, STXGrantRevokedException, STXInvalidTokenException, STXInsufficientScopeException The errors above.

Sideline is a working app built on this module: linking and unlinking, calls and live channels for the member, and public market data as the app.

v1.5.9Changelogllms.txtllms-full.txt