Complete documentation for integrating Clawdbot with Tlon Messenger (built on Urbit).
This extension enables Clawdbot to:
- Monitor and respond to direct messages on Tlon Messenger
- Monitor and respond to group channel messages when mentioned
- Auto-discover available group channels
- Use per-conversation subscriptions for reliable message delivery
- Automatic AI model fallback - Seamlessly switches from Anthropic to OpenAI when rate limited (see FALLBACK.md)
Ship: ~sitrul-nacwyl Test User: ~malmur-halmex
index.js- Plugin entry point, registers the Tlon channel adaptermonitor.js- Core monitoring logic, handles incoming messages and AI dispatchurbit-sse-client.js- Custom SSE client for Urbit HTTP APIcore-bridge.js- Dynamic loader for clawdbot core modulespackage.json- Plugin package definitionFALLBACK.md- AI model fallback system documentation
- Authentication: Uses ship name + code to authenticate via
/~/loginendpoint - Channel Creation: Creates Tlon Messenger channel via PUT to
/~/channel/{uid} - Activation: Sends "helm-hi" poke to activate channel (required!)
- Subscriptions:
- DMs: Individual subscriptions to
/dm/{ship}for each conversation - Groups: Individual subscriptions to
/{channelNest}for each channel
- DMs: Individual subscriptions to
- SSE Stream: Opens server-sent events stream for real-time updates
- Auto-Reconnection: Automatically reconnects if SSE stream dies
- Exponential backoff (1s to 30s delays)
- Up to 10 reconnection attempts
- Generates new channel ID on each attempt
- Auto-Discovery: Queries
/groups-ui/v6/init.jsonto find all available channels - Dynamic Refresh: Polls every 2 minutes for new conversations/channels
- Message Processing: When bot is mentioned, routes to AI via clawdbot core
- AI Fallback: Automatically switches providers when rate limited
- Primary: Anthropic Claude Sonnet 4.5
- Fallbacks: OpenAI GPT-4o, GPT-4 Turbo
- Automatic cooldown management
- See FALLBACK.md for details
cd ~/.clawdbot/extensions/tlon
npm installEdit ~/.clawdbot/clawdbot.json:
{
"channels": {
"tlon": {
"enabled": true,
"ship": "your-ship-name",
"code": "your-ship-code",
"url": "https://your-ship-name.tlon.network",
"showModelSignature": false,
"dmAllowlist": ["~friend-ship-1", "~friend-ship-2"],
"defaultAuthorizedShips": ["~malmur-halmex"],
"authorization": {
"channelRules": {
"chat/~host-ship/channel-name": {
"mode": "open",
"allowedShips": []
},
"chat/~another-host/private-channel": {
"mode": "restricted",
"allowedShips": ["~malmur-halmex", "~sitrul-nacwyl"]
}
}
}
}
}
}Configuration Options:
enabled- Enable/disable the Tlon channel (default:false)ship- Your Urbit ship name (required)code- Your ship's login code (required)url- Your ship's URL (required)showModelSignature- Append model name to responses (default:false)- When enabled, adds
[Generated by Claude Sonnet 4.5]to the end of each response - Useful for transparency about which AI model generated the response
- When enabled, adds
dmAllowlist- Ships allowed to send DMs (optional)- If omitted or empty, all DMs are accepted (default behavior)
- Ship names can include or omit the
~prefix - Example:
["~trusted-friend", "~another-ship"] - Blocked DMs are logged for visibility
defaultAuthorizedShips- Ships authorized in new/unconfigured channels (default:["~malmur-halmex"])- New channels default to
restrictedmode using these ships
- New channels default to
authorization- Per-channel access control (optional)channelRules- Map of channel nest to authorization rulesmode:"open"(all ships) or"restricted"(allowedShips only)allowedShips: Array of authorized ships (only forrestrictedmode)
For localhost development:
"url": "http://localhost:8080"For Tlon-hosted ships:
"url": "https://{ship-name}.tlon.network"The monitor needs to find clawdbot's core modules. Set the environment variable:
export CLAWDBOT_ROOT=/opt/homebrew/lib/node_modules/clawdbotOr if clawdbot is installed elsewhere:
export CLAWDBOT_ROOT=$(dirname $(dirname $(readlink -f $(which clawdbot))))Make it permanent (add to ~/.zshrc or ~/.bashrc):
echo 'export CLAWDBOT_ROOT=/opt/homebrew/lib/node_modules/clawdbot' >> ~/.zshrcThe bot needs API credentials to generate responses.
Option A: Use Claude Code CLI credentials
clawdbot agents add main
# Select "Use Claude Code CLI credentials"Option B: Use Anthropic API key
clawdbot agents add main
# Enter your API key from console.anthropic.comCLAWDBOT_ROOT=/opt/homebrew/lib/node_modules/clawdbot clawdbot gatewayOr create a launch script:
cat > ~/start-clawdbot.sh << 'EOF'
#!/bin/bash
export CLAWDBOT_ROOT=/opt/homebrew/lib/node_modules/clawdbot
clawdbot gateway
EOF
chmod +x ~/start-clawdbot.sh- Send a DM from another ship to ~sitrul-nacwyl
- Mention the bot:
~sitrul-nacwyl hello there! - Bot should respond with AI-generated reply
Check gateway logs:
tail -f /tmp/clawdbot/clawdbot-$(date +%Y-%m-%d).logLook for these indicators:
[tlon] Successfully authenticated to https://...[tlon] Auto-discovered N chat channel(s)[tlon] Connected! All subscriptions active[tlon] Received DM from ~ship: "..." (mentioned: true)[tlon] Dispatching to AI for ~ship (DM)[tlon] Delivered AI reply to ~ship
The bot automatically discovers and subscribes to all group channels using delta-based discovery for efficiency.
How Auto-Discovery Works:
- On startup: Fetches changes from the last 5 days via
/groups-ui/v5/changes/~YYYY.M.D..20.19.51..9b9d.json - Periodic refresh: Checks for new channels every 2 minutes
- Smart caching: Only fetches deltas, not full state each time
Benefits:
- Reduced bandwidth usage
- Faster startup (especially for ships with many groups)
- Automatically picks up new channels you join
- Context of recent group activity
Manual Configuration:
To disable auto-discovery and use specific channels:
{
"channels": {
"tlon": {
"enabled": true,
"ship": "your-ship-name",
"code": "your-ship-code",
"url": "https://your-ship-name.tlon.network",
"autoDiscoverChannels": false,
"groupChannels": [
"chat/~host-ship/channel-name",
"chat/~another-host/another-channel"
]
}
}
}The bot can append the AI model name to each response for transparency. Enable this feature in your config:
{
"channels": {
"tlon": {
"enabled": true,
"ship": "your-ship-name",
"code": "your-ship-code",
"url": "https://your-ship-name.tlon.network",
"showModelSignature": true
}
}
}Example output with signature enabled:
User: ~sitrul-nacwyl explain quantum computing
Bot: Quantum computing uses quantum mechanics principles like superposition
and entanglement to perform calculations...
[Generated by Claude Sonnet 4.5]
Supported model formats:
Claude Opus 4.5Claude Sonnet 4.5GPT-4oGPT-4 TurboGemini 2.0 Flash
When using the AI fallback system, signatures automatically reflect which model generated the response (e.g., if Anthropic is rate limited and OpenAI is used, the signature will show GPT-4o).
The bot can summarize recent channel activity when asked. This is useful for catching up on conversations you missed.
Trigger phrases:
~bot-ship summarize this channel~bot-ship what did I miss?~bot-ship catch me up~bot-ship tldr~bot-ship channel summary
Example:
User: ~sitrul-nacwyl what did I miss?
Bot: Here's a summary of the last 50 messages:
Main topics discussed:
1. Discussion about Urbit networking (Ames protocol)
2. Planning for next week's developer meetup
3. Bug reports for the new UI update
Key decisions:
- Meetup scheduled for Thursday at 3pm EST
- Priority on fixing the scrolling issue
Notable participants: ~malmur-halmex, ~bolbex-fogdys
How it works:
- Fetches the last 50 messages from the channel
- Sends them to the AI for summarization
- Returns a concise summary with main topics, decisions, and action items
The bot automatically maintains context in threaded conversations. When you mention the bot in a reply thread, it will respond within that thread instead of posting to the main channel.
Example:
Main channel post:
User A: ~sitrul-nacwyl what's the capital of France?
Bot: Paris is the capital of France.
└─ User B (in thread): ~sitrul-nacwyl and what's its population?
└─ Bot (in thread): Paris has a population of approximately 2.2 million...
Benefits:
- Keeps conversations organized
- Reduces noise in main channel
- Maintains conversation context within threads
Technical Details: The bot handles both top-level posts and thread replies with different data structures:
- Top-level posts:
response.post.r-post.set.essay - Thread replies:
response.post.r-post.reply.r-reply.set.memo
When replying in a thread, the bot uses the parent-id from the incoming message to ensure the reply stays within the same thread.
Note: Thread support is automatic - no configuration needed.
The bot can fetch and summarize web content when you share links.
Example:
User: ~sitrul-nacwyl can you summarize this https://example.com/article
Bot: This article discusses... [summary of the content]
How it works:
- Bot extracts URLs from rich text messages (including inline links)
- Fetches the web page content
- Summarizes using the WebFetch tool
Control which ships can invoke the bot in specific group channels. New channels default to restricted mode for security.
DMs: Always open (no restrictions)
Group Channels: Restricted by default, only ships in defaultAuthorizedShips can invoke the bot
{
"channels": {
"tlon": {
"enabled": true,
"ship": "sitrul-nacwyl",
"code": "your-code",
"url": "https://sitrul-nacwyl.tlon.network",
"defaultAuthorizedShips": ["~malmur-halmex"],
"authorization": {
"channelRules": {
"chat/~bitpyx-dildus/core": {
"mode": "open"
},
"chat/~nocsyx-lassul/bongtable": {
"mode": "restricted",
"allowedShips": ["~malmur-halmex", "~sitrul-nacwyl"]
}
}
}
}
}
}open - Any ship can invoke the bot when mentioned
- Good for public channels
- No
allowedShipsneeded
restricted (default) - Only specific ships can invoke the bot
- Good for private/work channels
- Requires
allowedShipslist - New channels use
defaultAuthorizedShipsif no rule exists
Make a channel public:
"chat/~bitpyx-dildus/core": {
"mode": "open"
}Restrict to specific users:
"chat/~nocsyx-lassul/bongtable": {
"mode": "restricted",
"allowedShips": ["~malmur-halmex"]
}New channel (no config):
- Mode:
restricted(safe default) - Allowed ships:
defaultAuthorizedShips(e.g.,["~malmur-halmex"])
Authorized mention:
~malmur-halmex: ~sitrul-nacwyl tell me about quantum computing
Bot: [Responds with answer]
Unauthorized mention (silently ignored):
~other-ship: ~sitrul-nacwyl tell me about quantum computing
Bot: [No response, logs show access denied]
Check logs:
tail -f /tmp/tlon-fallback.log | grep "Access"You'll see:
[tlon] ✅ Access granted: ~malmur-halmex in chat/~host/channel (authorized user)
[tlon] ⛔ Access denied: ~other-ship in chat/~host/channel (restricted, allowed: ~malmur-halmex)
-
Login (POST
/~/login)- Sends
password={code} - Returns authentication cookie in
set-cookieheader
- Sends
-
Channel Creation (PUT
/~/channel/{channelId})- Channel ID format:
{timestamp}-{random} - Body: array of subscription objects
- Response: 204 No Content
- Channel ID format:
-
Channel Activation (PUT
/~/channel/{channelId})- Critical: Must send helm-hi poke BEFORE opening SSE stream
- Poke structure:
{ "id": timestamp, "action": "poke", "ship": "sitrul-nacwyl", "app": "hood", "mark": "helm-hi", "json": "Opening API channel" }
-
SSE Stream (GET
/~/channel/{channelId})- Headers:
Accept: text/event-stream - Returns Server-Sent Events
- Format:
id: {event-id} data: {json-payload}
- Headers:
- Path:
/dm/{ship} - App:
chat - Event Format:
{ "id": "~ship/timestamp", "whom": "~other-ship", "response": { "add": { "memo": { "author": "~sender-ship", "sent": 1768742460781, "content": [ { "inline": [ "text", {"ship": "~mentioned-ship"}, "more text", {"break": null} ] } ] } } } }
- Path:
/{channelNest} - Channel Nest Format:
chat/~host-ship/channel-name - App:
channels - Event Format:
{ "response": { "post": { "id": "message-id", "r-post": { "set": { "essay": { "author": "~sender-ship", "sent": 1768742460781, "kind": "/chat", "content": [...] } } } } } }
Message content uses inline format with mixed types:
- Strings: plain text
- Objects with
ship: mentions (e.g.,{"ship": "~sitrul-nacwyl"}) - Objects with
break: line breaks (e.g.,{"break": null})
Example:
{
"inline": [
"Hey ",
{"ship": "~sitrul-nacwyl"},
" how are you?",
{"break": null},
"This is a new line"
]
}Extracts to: "Hey ~sitrul-nacwyl how are you?\nThis is a new line"
Simple includes check (case-insensitive):
const normalizedBotShip = botShipName.startsWith("~")
? botShipName
: `~${botShipName}`;
return messageText.toLowerCase().includes(normalizedBotShip.toLowerCase());Note: Word boundaries (\b) don't work with ~ character.
Cause: Some clawdbot dependencies (axios, Slack SDK) expect browser globals
Fix: Window.location polyfill is already added to monitor.js (lines 1-18)
Cause: core-bridge.js can't find clawdbot installation
Fix: Set CLAWDBOT_ROOT environment variable:
export CLAWDBOT_ROOT=/opt/homebrew/lib/node_modules/clawdbotCause: Trying to open SSE stream without activating channel first
Fix: Send helm-hi poke before opening stream (urbit-sse-client.js handles this)
Cause: Wrong subscription path or app name
Fix:
- DMs: Use
/dm/{ship}withapp: "chat" - Groups: Use
/{channelNest}withapp: "channels"
Cause: Not handling inline content objects properly
Fix: Text extraction handles mentions and breaks (monitor.js extractMessageText())
Cause: Message doesn't contain bot's ship name
Debug:
tail -f /tmp/clawdbot/clawdbot-*.log | grep "mentioned:"Should show:
[tlon] Received DM from ~malmur-halmex: "~sitrul-nacwyl hello..." (mentioned: true)
Cause: AI authentication not configured
Fix: Run clawdbot agents add main and configure credentials
Fix:
# Stop existing instance
clawdbot daemon stop
# Or force kill
lsof -ti:18789 | xargs kill -9Cause: Urbit SSE stream disconnected (sent "quit" event or stream ended)
Symptoms:
- Logs show:
[SSE] Received event: {"id":X,"response":"quit"} - No more incoming SSE events
- Bot appears online but doesn't respond to mentions
Fix: The bot now automatically reconnects! Look for these log messages:
[SSE] Stream ended, attempting reconnection...
[SSE] Reconnection attempt 1/10 in 1000ms...
[SSE] Reconnecting with new channel ID: xxx-yyy
[SSE] Reconnection successful!
Manual restart if needed:
kill $(pgrep -f "clawdbot gateway")
CLAWDBOT_ROOT=/opt/homebrew/lib/node_modules/clawdbot clawdbot gatewayConfiguration options (in urbit-sse-client.js constructor):
new UrbitSSEClient(url, cookie, {
autoReconnect: true, // Default: true
maxReconnectAttempts: 10, // Default: 10
reconnectDelay: 1000, // Initial delay: 1s
maxReconnectDelay: 30000, // Max delay: 30s
onReconnect: async (client) => {
// Optional callback for resubscription logic
}
})You can test the Urbit API directly:
import { UrbitSSEClient } from "./urbit-sse-client.js";
const api = new UrbitSSEClient(
"https://sitrul-nacwyl.tlon.network",
"your-cookie-here"
);
// Subscribe to DMs
await api.subscribe({
app: "chat",
path: "/dm/malmur-halmex",
event: (data) => console.log("DM:", data),
err: (e) => console.error("Error:", e),
quit: () => console.log("Quit")
});
// Connect
await api.connect();
// Send a DM
await api.poke({
app: "chat",
mark: "chat-dm-action",
json: {
ship: "~malmur-halmex",
diff: {
id: `~sitrul-nacwyl/${Date.now()}`,
delta: {
add: {
memo: {
content: [{ inline: ["Hello!"] }],
author: "~sitrul-nacwyl",
sent: Date.now()
},
kind: null,
time: null
}
}
}
}
});Enable verbose logging in urbit-sse-client.js:
// Line 169-171
if (parsed.response !== "subscribe" && parsed.response !== "poke") {
console.log("[SSE] Received event:", JSON.stringify(parsed).substring(0, 500));
}Remove the condition to see all events:
console.log("[SSE] Received event:", JSON.stringify(parsed).substring(0, 500));Format: {type}/{host-ship}/{channel-name}
Examples:
chat/~bitpyx-dildus/corechat/~malmur-halmex/v3aedb3schat/~sitrul-nacwyl/tm-wayfinding-group-chat
Parse with:
const match = channelNest.match(/^([^/]+)\/([^/]+)\/(.+)$/);
const [, type, hostShip, channelName] = match;Query: GET /~/scry/groups-ui/v6/init.json
Response structure:
{
"groups": {
"group-id": {
"channels": {
"chat/~host/name": { ... },
"diary/~host/name": { ... }
}
}
}
}Filter for chat channels only:
if (channelNest.startsWith("chat/")) {
channels.push(channelNest);
}- ✅ Plugin structure and registration
- ✅ Authentication and cookie management
- ✅ Channel creation and activation (helm-hi poke)
- ✅ SSE stream connection
- ✅ DM subscription and event parsing
- ✅ Group channel support
- ✅ Auto-discovery of channels
- ✅ Per-conversation subscriptions
- ✅ Text extraction (mentions and breaks)
- ✅ Mention detection
- ✅ Node.js polyfills (window.location)
- ✅ Core module integration
- ⏳ API authentication (user needs to configure)
- Helm-hi requirement: Must send helm-hi poke before opening SSE stream
- Subscription paths: Frontend uses
/v3globally, but individual/dm/{ship}and/{channelNest}paths work better - Event formats: V3 API uses
essayandmemostructures (not olderwritsformat) - Inline content: Mixed array of strings and objects (mentions, breaks)
- Tilde handling: Ship mentions already include
~prefix - Word boundaries:
\bregex doesn't work with~character - Browser globals: axios and Slack SDK need window.location polyfill
- Module resolution: Need CLAWDBOT_ROOT for dynamic imports
- Tlon Apps GitHub: https://github.com/tloncorp/tlon-apps
- Urbit HTTP API: @urbit/http-api package
- Tlon Frontend Code:
/tmp/tlon-apps/packages/shared/src/api/chatApi.ts - Clawdbot Docs: https://docs.clawd.bot/
- Anthropic Provider: https://docs.clawd.bot/providers/anthropic
- Support for message reactions
- Support for message editing/deletion
- Support for attachments/images
- Typing indicators
- Read receipts
- Message threading
- Channel-specific bot personas
- Rate limiting
- Message queuing for offline ships
- Metrics and monitoring
Built for integrating Clawdbot with Tlon messenger.
Developer: Claude (Sonnet 4.5) Platform: Tlon Messenger built on Urbit