The SDK exposes two API layers:
MetatellClient: a high-level client for common bot use cases.AgentClient: a lower-level client for advanced control and custom integrations.Use the generated TypeDoc reference at https://sdk.metatell.io/bot/ for exact signatures.
createMetatellClient(options)Creates a high-level MetatellClient. It throws ProtocolError when
serverUrl or roomId is missing.
import { createMetatellClient } from '@metatell/bot-sdk'
const client = createMetatellClient({
serverUrl: 'wss://metatell.app',
roomId: 'YOUR_ROOM_ID',
username: 'GuideBot',
authToken: process.env.METATELL_TOKEN,
})
| Option | Description |
|---|---|
serverUrl |
WebSocket origin for the metatell environment. Use an origin such as wss://metatell.app; do not include a room path. |
roomId |
Target room ID. |
authToken |
Optional OIDC access token sent when joining the room. It authenticates the bot so it receives room-role permissions such as text chat. |
username |
Optional bot display name. Defaults to MetatellBot. |
avatarId |
Optional avatar ID. When omitted, the first organization avatar is used. |
avatarSrc |
Optional GLTF URL used when avatarId is an organization avatar UUID. |
defaultAvatarId |
Optional fallback avatar ID used when neither avatarId nor an organization avatar is available. |
debug |
Enables verbose SDK logs. |
await client.connect()
await client.disconnect()
const status = client.getStatus() // { connected, connecting }
const info = await client.getInfo()
const sessionId = client.getSessionId()
connect() accepts optional ConnectOptions:
| Option | Description |
|---|---|
mode |
'enter' (default) joins the room and spawns the avatar. 'join-only' joins without spawning an avatar. |
initialPosition |
Spawn position for 'enter' mode. It cannot be combined with 'join-only'. |
expectedSceneIdentity |
Rejects with a NavigationError (SCENE_CHANGED) when the room's current scene does not match. |
connect() rejects with AuthenticationError for authentication failures,
TransportError for other connection failures, and NavigationError for a
scene mismatch.
getInfo() returns the bot name, a version string, the room ID, and the
session ID when the session is available.
await client.chat.send('Hello from a bot.')
client.chat.onMessage(async ({ from, text, mention, reply }) => {
if (mention) {
await reply(`Hi ${from.name}.`)
}
})
The message handler receives every chat message in the room:
| Field | Description |
|---|---|
from |
User that sent the message. |
text |
Message text with the mention token removed. |
mention |
{ sessionId, name } when the message contains a [@name](session-id) mention. |
reply(text) |
Sends a chat message to the room. |
const users = await client.room.getUsers()
const nearby = await client.room.getNearbyUsers(10)
const cachedUsers = client.getUsers()
room.getUsers() returns the current room users asynchronously. getUsers()
returns the same local cache synchronously. getNearbyUsers(radius) uses a
default radius of 10 and returns all users while the bot avatar is not spawned.
await client.avatar.select('avatar-asset-id')
await client.avatar.moveTo({ x: 1, y: 1.6, z: -2 })
await client.avatar.rotateTo({ x: 0, y: 180, z: 0 })
await client.avatar.lookAt({ x: 0, y: 1.6, z: 0 })
const position = client.avatar.getPosition()
const assets = await client.avatar.getAvailableAssets()
const animations = await client.avatar.getAvailableAnimations()
await client.avatar.play({ id: 'walking', loop: false })
Positions are expressed in room coordinates (meters). Rotations are Euler
angles in degrees. Only idle and walking are presets; avatar-specific
animation IDs must come from getAvailableAnimations(). play() requires an
id; URL-based animations are not supported. An unknown animation rejects with
a MetatellError whose code is ANIMATION_NOT_FOUND.
room.getSceneInfo() returns the scene reported when the bot joined the room.
room.prepareNavigation() fetches and parses the scene GLB, and
createNavigationRuntime() from @metatell/bot-sdk/navigation samples points,
projects positions, and finds paths on the navmesh. See the
@metatell/bot-sdk package README for the full
workflow, allowed origins, and protected-scene handling.
client.on('connected', () => {})
client.on('disconnected', () => {})
client.on('chat-message', ({ from, text, mention }) => {})
client.on('message', (data) => {})
client.on('user-join', (user) => {})
client.on('user-leave', (user) => {})
client.on('user-moved', (user) => {
console.log(user.name, user.position)
})
client.on('voice:mute-changed', ({ muted }) => {})
client.on('room-scene-changed', ({ previousIdentity, current }) => {})
Use client.off(event, listener) to unsubscribe. message receives raw room
message data; chat-message receives parsed chat messages.
Position updates are pushed over NAF. user-moved and getNearbyUsers() use
the presence session ID (same space as user-join / user-leave) when it can
be resolved. getNearbyUsers() reads a snapshot of the same cache.
Errors are reported by rejected promises. MetatellClient does not emit an
error event.
client.setRateLimit('messages', 2)
const current = client.getRateLimit('messages')
Supported keys are messages, moves, and looks. setRateLimit() stores a
per-second value that getRateLimit() returns. The high-level client does not
currently throttle calls based on these values; use TokenBucketRateLimiter or
RateLimitedQueue to throttle your own loops. AgentClient applies these
limits to send(), move(), look(), and lookAtNearest().
Voice transport is provided through @metatell/bot-realtime and the
enableVoice helper exported by @metatell/bot-sdk. Call it after
connect(), because the LiveKit token request uses the bot session ID.
import { createMetatellClient, enableVoice } from '@metatell/bot-sdk'
const client = createMetatellClient({ serverUrl, roomId, authToken })
await client.connect()
const voice = await enableVoice(client, {
transport: { type: 'livekit' },
handlers: {
onRemotePcm: async (pcm, meta) => {
console.log('audio frame from', meta.fromIdentity, pcm.length)
},
getLocalPcmStream: async function* () {
while (true) {
yield new Int16Array(960)
await new Promise((resolve) => setTimeout(resolve, 20))
}
},
},
})
await voice.detach()
Voice audio is signed 16-bit PCM, 48000 Hz, mono. onRemotePcm receives frames
in that format, and local audio must be supplied in that format. The
sampleRate option does not resample; use pcm.resample() to convert other
rates. Frames are 20 ms (960 samples) by default, or 10 ms (480 samples) with
frameDurationMs: 10. Chunks from getLocalPcmStream are split into frames
automatically, while client.sendVoiceFrame() requires exact frame sizes.
| Option | Description |
|---|---|
handlers |
onRemotePcm and getLocalPcmStream. |
transport.type |
'livekit', 'mock', or 'auto' (default; mock when NODE_ENV is test, otherwise livekit). |
frameDurationMs |
20 (default) or 10. |
autoStartPublish |
Starts publishing getLocalPcmStream automatically. Defaults to true. |
The LiveKit URL is derived from serverUrl for metatell.app and
metatell-stg.app. Set the METATELL_REALTIME_URL environment variable to use
a different LiveKit URL.
AgentClientAgentClient exposes lower-level operations for integrations that need direct
control over connection, room membership, avatar updates, animation, and voice.
createAgentClient() takes a core BotConfiguration:
import { createAgentClient } from '@metatell/bot-sdk'
const agent = createAgentClient(
{
serverUrl: 'wss://metatell.app',
hubUrl: 'https://metatell.app',
hubId: 'YOUR_ROOM_ID',
profile: { displayName: 'AgentBot', avatarId: 'YOUR_AVATAR_ID' },
authToken: process.env.METATELL_TOKEN,
},
{ rateLimit: { messages: 2 } },
)
await agent.connect({ url: 'https://metatell.app/YOUR_ROOM_ID' })
Use createAgentClientWithFactory(factory, config) to reuse an existing
CoreServiceFactory. Read the session ID from getStatus().sessionId;
getSessionId() exists on DefaultAgentClient but not on the AgentClient
interface type.
Common methods:
| Area | Methods |
|---|---|
| Connection | connect(options), disconnect(), join(room), leave(), getStatus() |
| Chat | send(text) |
| Avatar | move(position), look(target), lookAtNearest() |
| Users | getUsers(), getUser(id), getUsersNearby(radius) |
| Animation | playAnimation(id, options), stopAnimation(), getCurrentAnimation(), getAvailableAnimations() |
| Voice | sendVoiceFrame(frame), muteVoice(muted), isVoiceMuted() |
| Rate limits | setRateLimit(key, perSecond), getRateLimit(key) |
AgentClientEvents declares connection, room, user, message, avatar, and voice
event names. DefaultAgentClient currently emits voice:mute-changed itself;
subscribe to the core EventBus service for other core events.
The SDK exports a shared error hierarchy based on MetatellError:
| Class | code |
Description |
|---|---|---|
AuthenticationError |
AUTH_ERROR |
Authentication failed. |
TransportError |
TRANSPORT_ERROR |
Connection or transport failure. |
ProtocolError |
PROTOCOL_ERROR |
Invalid configuration or protocol data. |
TimeoutError |
TIMEOUT_ERROR |
Operation timed out. |
RateLimitedError |
RATE_LIMITED |
Request was rate limited. |
NavigationError |
NavigationErrorCode |
Scene fetch, parse, or navmesh failure. |
Use isMetatellError(error) to detect SDK errors and isRetryableError(error)
to check whether a retry may succeed.
import { AuthenticationError, TransportError, isRetryableError } from '@metatell/bot-sdk'
try {
await client.connect()
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('Authentication failed:', error.message)
} else if (error instanceof TransportError) {
console.error('Connection failed:', error.reason ?? error.message)
} else if (isRetryableError(error)) {
// Retry later.
} else {
throw error
}
}
pcm.resample(input, fromHz, toHz): resamples 16-bit PCM with linear
interpolation and yields Int16Array chunks.pcm.chunk(input, samplesPerChunk): splits an AsyncIterable<Int16Array>
into fixed-size chunks.TokenBucketRateLimiter and RateLimitedQueue: token-bucket throttling
helpers.Frequently used exported types include:
Vec3EulerUserBotInfoAvatarAssetAnimationCreateClientOptionsConnectOptionsMetatellClientEventsRoomSceneInfoRoomSceneChangedEventPrepareNavigationOptionsPrepareNavigationResultNavigationSnapshotNavigationRuntimeAgentVoiceConfigAgentVoiceAttachmentPcmInputThe SDK also re-exports typed NAF helpers from @metatell/bot-core. See
NAF messages.