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,
});
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,
});
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
const sessions = await project.sessions.list();
for (const s of sessions) {
console.log(`[${s.status}] ${s.id}`);
}
const session = await project.sessions.create({
title: "Script Test",
externalRoomId: "hello",
idleTimeout: 60,
});
await new Promise((r) => setTimeout(r, 30_000));
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",
sanctionPush: true,
});
Watch a session and its transcript live
const sessionId = process.env.SESSION_ID ?? "__YOUR_SESSION_ID__";
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}`);
});
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"],
});
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
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}`);
const sanction = await project.sanctions.create({
externalUserId: "user-123",
type: "mute",
reason: "Spamming voice chat",
endAt: new Date(Date.now() + 15 * 60_000).toISOString(),
});
await sanction.revoke({ reason: "Resolved" });
Reading needs sanctions.read (or sanctions); creating and revoking needs sanctions.