Skip to main content

Overview

FERAL uses two WebSocket endpoints:
EndpointPortPurpose
/v1/session9090Client connections (dashboard, desktop, mobile)
/v1/node9091Hardware node connections (wristband, glasses, robots)
Both use JSON-encoded FeralMessage frames.

FeralMessage Format

Every message over either WebSocket follows this envelope:
{
  "id": "msg_a1b2c3d4",
  "type": "text_command",
  "timestamp": "2026-04-12T14:30:00Z",
  "source": "dashboard",
  "target": "brain",
  "payload": { ... },
  "metadata": { ... }
}
FieldTypeDescription
idstringUnique message ID (UUID or prefixed)
typestringMessage type (see tables below)
timestampISO 8601When the message was created
sourcestringSender identifier (dashboard, brain, wristband_a3f2, etc.)
targetstringIntended recipient (brain, dashboard, * for broadcast)
payloadobjectType-specific data
metadataobjectOptional context (session ID, trace ID, etc.)

Client Protocol (/v1/session)

Connection Lifecycle

1

Connect

const ws = new WebSocket("ws://localhost:9090/v1/session");
2

Handshake

The brain sends a session_init message with capabilities and current context.
{
  "type": "session_init",
  "payload": {
    "session_id": "sess_abc123",
    "brain_version": "0.9.0",
    "capabilities": ["text", "voice", "sdui", "hardware"],
    "autonomy_mode": "hybrid"
  }
}
3

Exchange messages

Client sends commands, brain responds. Both sides can initiate.
4

Disconnect

Either side can close the socket. The brain persists the session in memory.

Client → Brain Message Types

TypeDescriptionPayload
text_commandText input from the user{ "text": "..." }
audio_chunkStreamed audio bytes (base64){ "data": "...", "format": "pcm_16k" }
audio_endEnd of audio stream{}
sdui_actionUser clicked an SDUI button/form{ "component_id": "...", "action": "...", "value": "..." }
approval_responseUser approved/denied an action{ "request_id": "...", "approved": true }
context_updateClient sends ambient context (screen, location){ "screen": "...", "location": {...} }

Brain → Client Message Types

TypeDescriptionPayload
text_responseText reply from the brain{ "text": "...", "sources": [...] }
text_deltaStreaming text token{ "delta": "..." }
audio_chunkStreamed audio response (base64){ "data": "...", "format": "pcm_16k" }
audio_endEnd of audio response{}
sduiServer-driven UI component{ "component": "chart", "props": {...} }
approval_requestAction needs user approval{ "request_id": "...", "action": "...", "risk": "medium" }
status_updateBrain state change{ "event": "...", "data": {...} }
errorError message{ "code": "...", "message": "..." }

Hardware Protocol (/v1/node)

Connection Lifecycle

1

Connect

ws = await websockets.connect("ws://localhost:9091/v1/node")
2

Register

The node sends its device manifest immediately after connection.
{
  "type": "node_register",
  "payload": {
    "device_id": "wristband_a3f2",
    "device_type": "wristband",
    "name": "FERAL Wristband",
    "capabilities": ["heart_rate", "spo2", "skin_temp"],
    "protocol_version": "1.0"
  }
}
3

Stream data

The node pushes sensor data; the brain sends commands.
4

Disconnect

Node closes the socket. The brain marks the device as offline.

Node → Brain Message Types

TypeDescription
node_registerDevice manifest on connect
sensor_dataPeriodic sensor readings
command_resultResponse to a brain command
node_statusBattery, connectivity, error states

Brain → Node Message Types

TypeDescription
node_acceptedRegistration acknowledged
device_commandCommand to execute (move, grip, set_light, etc.)
config_updatePush new settings to the device
pingKeep-alive (node must respond with pong)

Example: Full Client Conversation

// 1. Client sends text command
→ { "id": "m1", "type": "text_command", "source": "dashboard",
    "payload": { "text": "What's my heart rate?" } }

// 2. Brain streams response
← { "id": "m2", "type": "text_delta", "source": "brain",
    "payload": { "delta": "Your heart " } }
← { "id": "m2", "type": "text_delta", "source": "brain",
    "payload": { "delta": "rate is 72 bpm." } }
← { "id": "m2", "type": "text_response", "source": "brain",
    "payload": { "text": "Your heart rate is 72 bpm.", "sources": ["wristband_a3f2"] } }

Keep-Alive

Both protocols use ping/pong frames. The brain sends a ping every 30 seconds. Nodes and clients must respond within 10 seconds or the connection is considered dead.