Snek RPC Protocol API Specification
retoor retoor@molodetz.nl
Version 1.0 — February 2026
Table of Contents
- Protocol Overview
- Connection
- Request/Response Format
- Authentication
- Channel Operations
- Messaging
- Events
- Visual Feedback
- Message Routing
- Streaming
- Image Handling
- Tool System
- Error Handling
- Constants Reference
1. Protocol Overview
The Snek RPC protocol is a JSON-based remote procedure call protocol transported over WebSocket. Clients send method invocations as JSON objects and receive corresponding responses matched by a unique call identifier. The server also pushes unsolicited events (messages, typing indicators, joins/leaves) over the same connection.
Key characteristics:
- Transport: WebSocket (RFC 6455)
- Encoding: JSON over WebSocket text frames
- Concurrency model: Multiplexed — multiple outstanding calls share one connection, matched by
callId - Direction: Bidirectional — client sends requests, server sends responses and pushes events
2. Connection
Endpoint
wss://snek.molodetz.nl/rpc.ws
Plaintext variant (development only):
ws://snek.molodetz.nl/rpc.ws
WebSocket Parameters
| Parameter | Value | Description |
|---|---|---|
| Heartbeat interval | 30 seconds | WebSocket ping/pong keepalive (WS_HEARTBEAT) |
| Request timeout | 190 seconds | Maximum wait for an RPC response (DEFAULT_REQUEST_TIMEOUT) |
| Receive retry delay | 1.0 seconds | Delay before retrying after a receive error (WS_RECEIVE_RETRY_DELAY) |
Connection Lifecycle
1. Open WebSocket to wss://snek.molodetz.nl/rpc.ws
2. Call login(username, password)
3. Call get_user(null) to retrieve authenticated user info
4. Call get_channels() to enumerate available channels
5. Enter receive loop — process events and messages
6. On disconnect — reconnect with exponential backoff
Reconnection Strategy
| Parameter | Value |
|---|---|
| Max retries | 3 (RECONNECT_MAX_RETRIES) |
| Initial delay | 1.0 seconds (RECONNECT_INITIAL_DELAY) |
| Backoff factor | 2.0x (RECONNECT_BACKOFF_FACTOR) |
Delays follow: 1s → 2s → 4s. After exhausting retries the client should re-establish the full connection from step 1.
3. Request/Response Format
Client Request
Every RPC call from client to server uses the following JSON structure:
{
"method": "<method_name>",
"args": [<positional_arg_1>, <positional_arg_2>, ...],
"kwargs": {<keyword_args>},
"callId": "<unique_identifier>"
}
| Field | Type | Required | Description |
|---|---|---|---|
method |
string | yes | RPC method name |
args |
array | yes | Positional arguments (may be empty []) |
kwargs |
object | yes | Keyword arguments (may be empty {}) |
callId |
string | yes | Client-generated unique ID to correlate response. Recommended: 16-character hex string (e.g., os.urandom(8).hex()) or UUID v4. |
Server Response
The server responds with a JSON object containing the same callId:
{
"callId": "<matching_call_id>",
"data": <result_value>
}
| Field | Type | Description |
|---|---|---|
callId |
string | Matches the request callId |
data |
any | Return value — object, array, string, or null depending on the method |
Additional top-level fields may be present depending on context (e.g., event, message, username).
Fire-and-Forget Requests
Some calls do not require a response. The client sends the request normally but does not wait for a matching callId response. This is a client-side optimization — the server may still send a response.
4. Authentication
login
Authenticate with the Snek platform.
Request:
{
"method": "login",
"args": ["<username>", "<password>"],
"kwargs": {},
"callId": "<id>"
}
Response:
{
"callId": "<id>",
"data": { ... }
}
Must be the first call after establishing the WebSocket connection.
get_user
Retrieve user information.
Request:
{
"method": "get_user",
"args": [null],
"kwargs": {},
"callId": "<id>"
}
Pass null as the sole argument to retrieve the authenticated user.
Response:
{
"callId": "<id>",
"data": {
"username": "botname",
"nick": "BotNick"
}
}
| Field | Type | Description |
|---|---|---|
username |
string | Login username (unique identifier) |
nick |
string | Display name |
5. Channel Operations
get_channels
Retrieve all channels visible to the authenticated user.
Request:
{
"method": "get_channels",
"args": [],
"kwargs": {},
"callId": "<id>"
}
Response:
{
"callId": "<id>",
"data": [
{
"uid": "channel_unique_id",
"name": "Channel Name",
"tag": "dm"
},
...
]
}
Channel Object
| Field | Type | Description |
|---|---|---|
uid |
string | Unique channel identifier. Matches pattern ^[a-zA-Z0-9_-]+$ |
name |
string | Human-readable channel name |
tag |
string | Channel type. "dm" indicates a direct message channel |
Channel Types
| Tag | Behavior |
|---|---|
"dm" |
Direct message — bot always responds to messages |
| other / none | Public channel — bot responds only if explicitly joined or triggered |
6. Messaging
send_message
Send a message to a channel.
Request:
{
"method": "send_message",
"args": ["<channel_uid>", "<message_text>", <is_final>],
"kwargs": {},
"callId": "<id>"
}
| Argument | Type | Required | Description |
|---|---|---|---|
channel_uid |
string | yes | Target channel UID |
message_text |
string | yes | Message content (Markdown supported) |
is_final |
boolean | yes | true for complete messages, false for streaming partial updates |
Response:
{
"callId": "<id>",
"data": { ... }
}
Drive URL Rewriting
Relative drive paths are rewritten before processing:
(/drive.bin → (https://snek.molodetz.nl/drive.bin
7. Events
The server pushes unsolicited events over the WebSocket connection. Events do not have a callId matching any pending request.
Event Structure
{
"event": "<event_type>",
"data": { ... }
}
Message Event
Messages appear as a special case — they carry top-level fields instead of being nested under data:
{
"event": "message",
"message": "Hello world",
"username": "sender_username",
"user_nick": "SenderNick",
"channel_uid": "target_channel_uid",
"is_final": true
}
| Field | Type | Description |
|---|---|---|
event |
string | "message" |
message |
string | Message content |
username |
string | Sender's username |
user_nick |
string | Sender's display name |
channel_uid |
string | Channel where the message was sent |
is_final |
boolean | true for complete messages, false for streaming updates |
Clients should ignore messages where is_final is false unless implementing a streaming display.
set_typing Event
Server-pushed event indicating a user is typing or providing visual feedback.
{
"event": "set_typing",
"data": { ... }
}
join / leave Events
{
"event": "join",
"data": {
"channel_uid": "<channel_uid>"
}
}
{
"event": "leave",
"data": {
"channel_uid": "<channel_uid>"
}
}
Event Handling Pattern
Events are dispatched to handler methods named on_<event_type>. The data object is unpacked as keyword arguments:
Event: {"event": "join", "data": {"channel_uid": "abc123"}}
Handler: on_join(channel_uid="abc123")
Unknown events are silently ignored.
8. Visual Feedback
set_typing
Set a colored typing indicator for the bot in a channel. Used for visual "thinking" feedback.
Request:
{
"method": "set_typing",
"args": ["<channel_uid>", "<html_color_code>"],
"kwargs": {},
"callId": "<id>"
}
| Argument | Type | Description |
|---|---|---|
channel_uid |
string | Target channel |
html_color_code |
string | HTML hex color code (e.g., "#FF0000") |
Color Generation
Random bright colors are generated using HSL with these ranges:
| Component | Min | Max |
|---|---|---|
| Hue | 0.0 | 1.0 |
| Saturation | 0.7 | 1.0 |
| Lightness | 0.5 | 0.7 |
The color must match the pattern ^#[0-9A-Fa-f]{6}$.
9. Message Routing
Incoming messages are classified and routed in the following priority order:
| Priority | Condition | Handler |
|---|---|---|
| 1 | username == self.username |
on_own_message(channel_uid, message) |
| 2 | Message starts with "ping" |
on_ping(username, user_nick, channel_uid, message_after_ping) |
| 3 | Message contains @nick join or @username join |
on_join(channel_uid) |
| 4 | Message contains @nick leave or @username leave |
on_leave(channel_uid) |
| 5 | Message contains @nick or @username |
on_mention(username, user_nick, channel_uid, message) |
| 6 | Default | on_message(username, user_nick, channel_uid, message) |
Channel Permission Rules
For on_message processing, the bot applies these rules:
- DM channels (
tag == "dm"): Always respond. - Public channels: Respond only if:
- The bot has been explicitly joined to the channel, OR
- The message matches a configured trigger pattern, OR
- The bot's username appears in the message text.
Ping/Pong
Messages starting with "ping" trigger an automatic "pong" response:
Incoming: "ping hello"
Response: "pong hello"
10. Streaming
The protocol supports incremental message delivery using the is_final field on send_message.
Sending Streaming Messages
1. send_message(channel_uid, "Partial cont...", false) ← partial update
2. send_message(channel_uid, "Partial content h...", false) ← partial update
3. send_message(channel_uid, "Partial content here.", true) ← final message
Each partial message replaces the previous one in the UI. The final message (is_final=true) marks the message as complete.
Receiving Streaming Messages
When receiving events, messages with is_final=false represent in-progress content from another user or bot. Standard bot implementations skip non-final messages and only process the final version:
if not data.is_final:
continue
Streaming Update Interval
The minimum interval between streaming updates is 0.0 seconds (STREAMING_UPDATE_INTERVAL), meaning updates are sent as fast as they are generated.
11. Image Handling
URL Extraction
Image URLs are extracted from message text using the pattern:
https?://\S+\.(?:png|jpg|jpeg|gif|bmp|webp|svg)(?:\?\S*)?
Matched URLs are stripped of trailing ., ', and " characters.
Image Formats
Two encoding formats are supported:
OpenAI Format (default)
Images are sent as separate content blocks in the OpenAI multi-modal message format:
{
"role": "user",
"content": [
{
"type": "text",
"text": "message text with URL removed"
},
{
"type": "image_url",
"image_url": {
"url": "data:image/png;base64,<base64_encoded_data>"
}
}
]
}
DeepSeek Format
Images are inlined into the text content as tagged references:
{
"role": "user",
"content": [
{
"type": "text",
"text": "message with [image: data:image/png;base64,<base64_data>] inline"
}
]
}
MIME Type Detection
MIME types are resolved in order:
- File extension via
mimetypes.guess_type() - Magic bytes detection:
| Bytes | MIME Type |
|---|---|
\x89PNG\r\n\x1a\n |
image/png |
\xff\xd8 |
image/jpeg |
GIF87a or GIF89a |
image/gif |
RIFF....WEBP |
image/webp |
- Fallback:
image/png
12. Tool System
The tool system follows the OpenAI function calling specification. Tools are serialized as function definitions, sent alongside the chat completion request, and executed when the LLM returns tool_calls.
Tool Definition Schema
Each tool is serialized as:
{
"type": "function",
"function": {
"name": "<method_name>",
"description": "<docstring>",
"parameters": {
"type": "object",
"properties": {
"<param_name>": {
"type": "<json_type>",
"default": "<default_value>"
}
},
"required": ["<param_without_default>"]
}
}
}
Type Mapping
| Python Type | JSON Schema Type |
|---|---|
str |
"string" |
int |
"integer" |
bool |
"boolean" |
list |
"array" |
dict |
"object" |
None |
"null" |
| (default) | "string" |
Parameters with default values are optional; parameters without defaults are listed in required.
Tool Call Flow
1. Client sends chat completion request with tools array
2. LLM returns response with tool_calls array
3. Client executes each tool call
4. Client sends tool results back as role:"tool" messages
5. Client sends another chat completion request with updated context
6. Repeat until LLM returns a text response without tool_calls
Tool Call Response (from LLM)
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "database_find",
"arguments": "{\"table\": \"users\", \"query\": {\"active\": true}}"
}
}
]
}
Tool Result Message
{
"role": "tool",
"tool_call_id": "call_abc123",
"content": "<stringified_result>"
}
Tool Call Limits
| Parameter | Value | Description |
|---|---|---|
| Max depth | 25 | Maximum consecutive tool call rounds (MAX_TOOL_CALL_DEPTH) |
| Max repeated calls | 5 | Maximum identical tool invocations (MAX_REPEATED_TOOL_CALLS) |
| Max consecutive errors | 3 | Abort tool loop after 3 consecutive errors |
Method Exclusions
Methods are excluded from tool serialization if:
- The name starts with
_ - The name is
serializeorhandle - The attribute is not callable
13. Error Handling
Exception Hierarchy
BotError
├── ConnectionError Connection/WebSocket failures
├── AuthenticationError Login failures
├── RPCError RPC call failures, timeouts
├── ToolError Tool execution failures
├── DatabaseError Database operation failures
├── ValidationError Input validation failures
└── CircuitBreakerOpenError Circuit breaker tripped
RPC Error Conditions
| Condition | Behavior |
|---|---|
| WebSocket closed/closing | RPCError raised |
| WebSocket error frame | RPCError raised |
| Response timeout (190s) | RPCError raised |
| Invalid JSON in response | Logged as warning, skipped |
| Connection closed during call | RPCError raised |
Reconnection on Send Failure
When send_message fails, the client retries with exponential backoff:
Attempt 1: send → fail → wait 1s
Attempt 2: reconnect + send → fail → wait 2s
Attempt 3: reconnect + send → fail → raise ConnectionError
Each reconnection attempt performs a full login sequence on a new WebSocket.
Circuit Breaker
Tool execution is protected by a circuit breaker pattern:
| Parameter | Value |
|---|---|
| Failure threshold | 5 consecutive failures |
| Recovery timeout | 60 seconds |
| Half-open max calls | 3 |
States: CLOSED → (threshold exceeded) → OPEN → (timeout elapsed) → HALF_OPEN → (success) → CLOSED
When the circuit breaker is open, tool calls fail immediately with CircuitBreakerOpenError.
14. Constants Reference
Connection
| Constant | Value | Description |
|---|---|---|
DEFAULT_WS_URL |
wss://snek.molodetz.nl/rpc.ws |
Default WebSocket endpoint |
DEFAULT_OPENAI_URL |
https://api.openai.com |
Default LLM API base URL |
DEFAULT_SEARCH_API_BASE |
https://search.molodetz.nl |
Default search API endpoint |
WS_HEARTBEAT |
30.0s | WebSocket heartbeat interval |
DEFAULT_REQUEST_TIMEOUT |
190s | RPC call timeout |
HTTP_CONNECT_TIMEOUT |
90.0s | HTTP TCP connect timeout |
Reconnection
| Constant | Value | Description |
|---|---|---|
RECONNECT_MAX_RETRIES |
3 | Maximum reconnection attempts |
RECONNECT_INITIAL_DELAY |
1.0s | First retry delay |
RECONNECT_BACKOFF_FACTOR |
2.0 | Delay multiplier per retry |
WS_RECEIVE_RETRY_DELAY |
1.0s | Delay after receive error |
Concurrency
| Constant | Value | Description |
|---|---|---|
MAX_CONCURRENT_OPERATIONS |
100 | WebSocket semaphore limit |
MAX_CONCURRENT_REQUESTS |
10 | HTTP connection pool limit |
THREAD_POOL_WORKERS |
8 | Thread pool for blocking operations |
PROCESS_POOL_WORKERS |
4 | Process pool for CPU-intensive tasks |
EXECUTOR_TIMEOUT |
30.0s | Thread/process pool call timeout |
Message Handling
| Constant | Value | Description |
|---|---|---|
MAX_MESSAGE_HISTORY |
50 | Recent messages retained in memory |
DEFAULT_OPENAI_LIMIT |
200 | Max messages sent to LLM per session |
CONTEXT_WINDOW_TOKEN_LIMIT |
120000 | Token budget for LLM context |
STREAMING_UPDATE_INTERVAL |
0.0s | Min interval between streaming updates |
Tool Limits
| Constant | Value | Description |
|---|---|---|
MAX_TOOL_CALL_DEPTH |
25 | Max consecutive tool call rounds |
MAX_REPEATED_TOOL_CALLS |
5 | Max identical tool invocations |
MAX_RETRY_ATTEMPTS |
3 | General retry limit |
Timing
| Constant | Value | Description |
|---|---|---|
TASK_CHECK_INTERVAL |
0.5s | Service loop tick interval |
THINKING_TASK_INTERVAL |
1.0s | Typing indicator refresh interval |
SERVICE_CLEANUP_INTERVAL |
3600s | Periodic cleanup cycle |
SHUTDOWN_TIMEOUT |
5.0s | Max wait for graceful shutdown |
MIN_COUNTER_UPDATE_INTERVAL |
240s | Debounce for counter updates |
Agentic
| Constant | Value | Description |
|---|---|---|
AGENTIC_MAX_RESEARCH_QUERIES |
8 | Max search queries per research task |
AGENTIC_MAX_PLAN_STEPS |
10 | Max steps in a generated plan |
AGENTIC_SCRATCHPAD_SIZE |
50 | Scratchpad buffer size |
Visual
| Constant | Value | Description |
|---|---|---|
COLOR_HUE_MIN |
0.0 | HSL hue range start |
COLOR_HUE_MAX |
1.0 | HSL hue range end |
COLOR_SATURATION_MIN |
0.7 | HSL saturation range start |
COLOR_SATURATION_MAX |
1.0 | HSL saturation range end |
COLOR_LIGHTNESS_MIN |
0.5 | HSL lightness range start |
COLOR_LIGHTNESS_MAX |
0.7 | HSL lightness range end |
Media
| Constant | Value | Description |
|---|---|---|
MEDIA_HUNT_DEDUP_MAX_SIZE |
10000 | Dedup cache max entries |
MEDIA_HUNT_DEDUP_TTL |
86400s | Dedup cache TTL (24 hours) |
MEDIA_HUNT_HEAD_TIMEOUT |
5.0s | HEAD request timeout |
MEDIA_HUNT_FETCH_TIMEOUT |
30.0s | Full fetch timeout |
Bot Name Sanitization
The following bot names are sanitized in outgoing messages to prevent cross-triggering:
snek, grok, snik, lisa, gemma, joanne, ira, thomas
Sanitization inserts a hyphen after the first character (e.g., snek → s-nek).
Appendix A: Complete Connection Example
CLIENT → WebSocket CONNECT wss://snek.molodetz.nl/rpc.ws
SERVER ← 101 Switching Protocols
CLIENT → {"method":"login","args":["mybot","mypassword"],"kwargs":{},"callId":"a1b2c3d4e5f6g7h8"}
SERVER ← {"callId":"a1b2c3d4e5f6g7h8","data":{...}}
CLIENT → {"method":"get_user","args":[null],"kwargs":{},"callId":"b2c3d4e5f6g7h8a1"}
SERVER ← {"callId":"b2c3d4e5f6g7h8a1","data":{"username":"mybot","nick":"MyBot"}}
CLIENT → {"method":"get_channels","args":[],"kwargs":{},"callId":"c3d4e5f6g7h8a1b2"}
SERVER ← {"callId":"c3d4e5f6g7h8a1b2","data":[{"uid":"ch1","name":"general","tag":"public"},{"uid":"ch2","name":"DM","tag":"dm"}]}
--- receive loop ---
SERVER ← {"event":"message","message":"hello @MyBot","username":"alice","user_nick":"Alice","channel_uid":"ch1","is_final":true}
CLIENT → {"method":"send_message","args":["ch1","hey, what's up?",true],"kwargs":{},"callId":"d4e5f6g7h8a1b2c3"}
SERVER ← {"callId":"d4e5f6g7h8a1b2c3","data":{...}}
Appendix B: Streaming Example
CLIENT → {"method":"set_typing","args":["ch1","#FF6B35"],"kwargs":{},"callId":"e5f6g7h8a1b2c3d4"}
CLIENT → {"method":"send_message","args":["ch1","Working on",false],"kwargs":{},"callId":"f6g7h8a1b2c3d4e5"}
CLIENT → {"method":"send_message","args":["ch1","Working on it...",false],"kwargs":{},"callId":"g7h8a1b2c3d4e5f6"}
CLIENT → {"method":"send_message","args":["ch1","Working on it... done!",true],"kwargs":{},"callId":"h8a1b2c3d4e5f6g7"}
Appendix C: Bot State Machine
INITIALIZING → INITIALIZED → CONNECTING → CONNECTED → RUNNING → SHUTTING_DOWN → SHUTDOWN
↓
ERROR
| State | Description |
|---|---|
INITIALIZING |
HTTP sessions, signal handlers, and background tasks being set up |
INITIALIZED |
Setup complete, ready to connect |
CONNECTING |
WebSocket connection in progress |
CONNECTED |
Authenticated and channels enumerated |
RUNNING |
Processing messages in the receive loop |
SHUTTING_DOWN |
Graceful shutdown initiated, cancelling background tasks |
SHUTDOWN |
All resources released |
ERROR |
Initialization or fatal runtime failure |