Skip to main content

SDK samples

Short, focused snippets for the most common tasks with the @4players/odin-cortex SDK. Each is runnable on its own — set CORTEX_API_KEY and your project id, then run with npx tsx <file>.ts.

Every sample starts from the same client:

import { CortexClient } from "@4players/odin-cortex";

const PROJECT_ID = "__YOUR_PROJECT_ID__";

const client = new CortexClient({
baseUrl: process.env.BASE_URL ?? "https://cortex.odin.4players.io",
apiKey: process.env.CORTEX_API_KEY, // ots_live_...
});

const project = client.project(PROJECT_ID);

Create and start a session​

const session = await project.sessions.create({
title: "Script Test Session",
externalRoomId: "room-1",
idleTimeout: 60, // seconds without participants before the session stops itself
});

console.log("Session created:", session.id);

The bot joins right away — autoStart defaults to true. To create the session now and start the bot later, pass autoStart: false and call start() yourself:

const later = await project.sessions.create({
title: "Starts later",
externalRoomId: "room-2",
autoStart: false,
});

await later.start();

List sessions, then collect a transcript​

// List existing sessions
const sessions = await project.sessions.list();
for (const s of sessions) {
console.log(`[${s.status}] ${s.id}`);
}

// Create (the bot starts automatically), wait, stop, read messages
const session = await project.sessions.create({
title: "Script Test",
externalRoomId: "hello",
idleTimeout: 60,
});

await new Promise((r) => setTimeout(r, 30_000)); // let it collect audio
await session.stop();

const messages = await session.getMessages();
console.log(`Got ${messages.length} message(s)`);

Read a transcript by cursor​

Every message has a per-session seq. listMessages() returns only what is newer than after; with ifNoneMatch an unchanged transcript resolves to null and costs almost nothing — ideal for polling, or for catching up after a reconnect.

const session = await project.sessions.get(sessionId);

let after = 0;
for (;;) {
const page = await session.listMessages(
{ after, limit: 200, include: ["annotations"] },
{ ifNoneMatch: `"${after}"` }
);
if (page) {
for (const m of page.messages) console.log(`#${m.seq} ${m.senderName}: ${m.content}`);
after = page.nextCursor ?? page.latestSeq;
}
await new Promise((r) => setTimeout(r, 2_000));
}

Update the bot’s user data​

The bot’s userData (the profile it presents in the ODIN room) lives in project settings. Use the partial update() to change just that one field — everything else is preserved:

const botUserData = {
name: "Transcription Bot",
role: "bot",
avatar: "https://example.com/bot.png",
};

const updated = await project.settings.update({ botUserData });
console.log("Updated bot userData:", updated.botUserData);

Use project.settings.upsert() instead when you want to set the full configuration (including the token provider).

The same partial update() switches Room Push on — live captions for everyone in the room, and sanction frames for in-room enforcement:

await project.settings.update({
transcriptPush: "all", // "off" (default) | "all" | "own"
sanctionPush: true, // only once your clients handle cortex.sanction / cortex.mutes
});

Watch a session and its transcript live​

const sessionId = process.env.SESSION_ID ?? "__YOUR_SESSION_ID__";

// The session document — fires on any change, undefined if deleted.
const sessionSub = project.sessions.watch(sessionId);
sessionSub.onSnapshot((session) => {
if (!session) return console.log("[session] deleted or not found");
console.log(`[session] ${session.title} — status=${session.status}`);
});

// The transcript — initial messages via REST, new ones streamed live.
const session = await project.sessions.get(sessionId);
const messageSub = session.watchMessages();
messageSub.onSnapshot((messages, changes) => {
for (const change of changes) {
if (change.type === "added") {
console.log(` + ${change.data.senderName}: ${change.data.content}`);
}
}
});

process.on("SIGINT", () => {
sessionSub.unsubscribe();
messageSub.unsubscribe();
process.exit(0);
});

Watch the whole sessions collection​

const sub = project.sessions.watch();

sub.onSnapshot((sessions, changes) => {
console.log(`\n[snapshot] ${sessions.length} session(s)`);
for (const change of changes) {
console.log(` ${change.type.padEnd(8)} ${change.id} (${change.data.status})`);
}
});

sub.onError((err) => console.error("[error]", err));

process.on("SIGINT", () => {
sub.unsubscribe();
process.exit(0);
});

Push a message into the room​

While a session is running, send your own data to the players in its ODIN room — the bot delivers it as a Room Push frame. The key needs the rooms.push scope.

const sent = await project.rooms.send(sessionId, {
type: "match.countdown",
data: { secondsLeft: 10 },
targetExternalUserIds: ["alice", "bob"], // optional — omit to reach everyone
});

console.log(`Frame ${sent.id} accepted (${sent.sizeBytes} bytes)`);

No bot in the room answers 409; more than 20 messages per second per room throws a CortexRateLimitError.

Decode room frames in a client​

In a web or Node client connected to the ODIN room, hand every message from the Cortex bot peer to decodeCortexRoomFrame(). It returns null for anything that isn’t a Cortex frame:

import { decodeCortexRoomFrame, CortexRoomFrameType } from "@4players/odin-cortex";

function onBotMessage(bytes: Uint8Array) {
const frame = decodeCortexRoomFrame(bytes);
if (!frame) return;

switch (frame.type) {
case CortexRoomFrameType.TRANSCRIPT:
console.log(`[caption] ${frame.data.text}`);
break;
case "match.countdown":
console.log(`[countdown] ${frame.data.secondsLeft}s`);
break;
}
}

Check and issue sanctions​

// Is this player banned or muted right now?
const active = await project.sanctions.getActive("user-123");
const banned = active.some((s) => s.type === "temp_ban" || s.type === "perm_ban");
console.log(`${active.length} active sanction(s), banned=${banned}`);

// Mute for 15 minutes — keyed on the user, so it applies in every session
const sanction = await project.sanctions.create({
externalUserId: "user-123",
type: "mute",
reason: "Spamming voice chat",
endAt: new Date(Date.now() + 15 * 60_000).toISOString(),
});

// Lift it again — the history is kept
await sanction.revoke({ reason: "Resolved" });

Reading needs sanctions.read (or sanctions); creating and revoking needs sanctions.