Messaging Protocol — Self-Hosted Matrix Stack
HARD RULE: All platform messaging (WhatsApp, Instagram, Slack, iMessage, Telegram, Signal, Discord) goes through the self-hosted Matrix stack. Never use the Beeper desktop app. Never use a third-party messaging MCP.
Infrastructure
| Component | URL | Notes |
|-----------|-----|-------|
| Matrix homeserver (Synapse) | http://localhost:8008 | Server name: rawgrowth.local |
| WhatsApp bridge (mautrix-whatsapp) | http://localhost:29318 | Logged in as +19493743016 |
| Meta bridge (Instagram/Facebook) | http://localhost:29319 | — |
| Slack bridge | http://localhost:29320 | — |
| Twitter bridge | http://localhost:29321 | — |
| Discord bridge | http://localhost:29322 | — |
All containers in /Users/scanbot/RawgrowthOS/engineering/apps/comms-agent/docker-compose.yml. They restart automatically (restart: unless-stopped), but require the Docker daemon (Colima) to be running.
Recovery procedure if Matrix is unreachable on localhost:8008:
colima start— if it fails with "disk in use by instance", runcolima stop --force, thenpkill -9 limactl, then retry.cd /Users/scanbot/RawgrowthOS/engineering/apps/comms-agent && docker compose up -d- Wait ~10s, verify:
curl -sS -o /dev/null -w "%{http_code}\n" http://localhost:8008/_matrix/client/versionsshould return 200. - Verify token:
curl -sS -H "Authorization: Bearer $MATRIX_TOKEN" http://localhost:8008/_matrix/client/v3/account/whoamireturns the user ID.
Agent Bot Credentials
| Field | Value |
|-------|-------|
| User ID | @agentbot:rawgrowth.local |
| Access token | syt_YWdlbnRib3Q_sXJzNktpYuSUJkCIufqd_2IFHMI |
| Password (for re-login if token expires) | RawgrowthAgentBot2026!! |
| Chris's Matrix user | @chris:rawgrowth.local |
| WhatsApp login ID | 19493743016 |
| WhatsApp management room | !rfEnUovczRzyOpqPYE:rawgrowth.local |
Token expiry: If the token ever returns M_UNKNOWN_TOKEN, re-login:
curl -s http://localhost:8008/_matrix/client/v3/login \
-H "Content-Type: application/json" \
-d '{"type":"m.login.password","user":"agentbot","password":"RawgrowthAgentBot2026!!","device_id":"agentbot-persistent"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])"
Sending a Message (Any Platform)
MATRIX_TOKEN="syt_YWdlbnRib3Q_sXJzNktpYuSUJkCIufqd_2IFHMI"
ROOM_ID="!ROOM_ID_HERE:rawgrowth.local"
TXN_ID="agent_$(date +%s%N)"
MSG="Your message here"
curl -s -X PUT \
"http://localhost:8008/_matrix/client/v3/rooms/$(python3 -c "import urllib.parse; print(urllib.parse.quote('$ROOM_ID'))")/send/m.room.message/$TXN_ID" \
-H "Authorization: Bearer $MATRIX_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"m.text\",\"body\":$(python3 -c "import json,sys; print(json.dumps(sys.argv[1]))" "$MSG")}"
Starting a New WhatsApp DM (no existing room)
CHRIS_TOKEN="syt_Y2hyaXM_QLaSjjFZzVvWvQXWgvUF_0kMdq0"
MGMT_ROOM="!rfEnUovczRzyOpqPYE:rawgrowth.local"
PHONE="19493743016" # WhatsApp login ID (no +)
TARGET="995595025574" # Target number — NO leading +, digits only
# Step 1: Resolve (verify on WhatsApp)
curl -s -X PUT \
"http://localhost:8008/_matrix/client/v3/rooms/$(python3 -c "import urllib.parse; print(urllib.parse.quote('$MGMT_ROOM'))")/send/m.room.message/txn_resolve_$(date +%s%N)" \
-H "Authorization: Bearer $CHRIS_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"m.text\",\"body\":\"resolve-identifier $PHONE $TARGET\"}"
sleep 3
# Step 2: Create the chat room
curl -s -X PUT \
"http://localhost:8008/_matrix/client/v3/rooms/$(python3 -c "import urllib.parse; print(urllib.parse.quote('$MGMT_ROOM'))")/send/m.room.message/txn_start_$(date +%s%N)" \
-H "Authorization: Bearer $CHRIS_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"m.text\",\"body\":\"start-chat $PHONE $TARGET\"}"
sleep 4
# Step 3: Read the bot's reply to get the new room ID
curl -s "http://localhost:8008/_matrix/client/v3/rooms/$(python3 -c "import urllib.parse; print(urllib.parse.quote('$MGMT_ROOM'))")/messages?dir=b&limit=3" \
-H "Authorization: Bearer $CHRIS_TOKEN" | python3 -c "
import sys, json
d = json.load(sys.stdin)
for e in d.get('chunk', []):
body = e.get('content', {}).get('body', '')
if 'Created chat' in body or 'matrix.to' in body:
print(body)
"
⚠️ Phone Number Format for WhatsApp
| Format | Works? |
|--------|--------|
| 19493743016 | ✅ US (no +) |
| 995595025574 | ✅ Georgia (no +) |
| +99595025574 | ❌ Fails |
| +995595025574 | ✅ Resolves but use without + for start-chat |
Rule: Always strip the leading + when sending to the bridge.
Checking for Unread Messages
import json, urllib.request, urllib.parse, time
URL = "http://localhost:8008"
TOKEN = "syt_YWdlbnRib3Q_sXJzNktpYuSUJkCIufqd_2IFHMI"
CHRIS_ID = "@chris:rawgrowth.local"
CUTOFF = int((time.time() - 14 * 86400) * 1000) # 14 days back
def api(path):
req = urllib.request.Request(f"{URL}{path}", headers={"Authorization": f"Bearer {TOKEN}"})
try:
return json.loads(urllib.request.urlopen(req, timeout=5).read())
except:
return {}
# Get Chris's rooms (agentbot must be joined, or use Chris's token)
rooms = api("/_matrix/client/v3/joined_rooms").get("joined_rooms", [])
for room_id in rooms:
msgs = api(f"/_matrix/client/v3/rooms/{urllib.parse.quote(room_id)}/messages?dir=b&limit=5")
last = next((m for m in msgs.get("chunk", []) if m.get("type") == "m.room.message"), None)
if not last:
continue
if last.get("sender") == CHRIS_ID:
continue # Chris already replied
if last.get("origin_server_ts", 0) < CUTOFF:
continue
print(room_id, last.get("content", {}).get("body", "")[:80])
Bridge Bot IDs (for filtering rooms by platform)
| Platform | Bridge Bot User ID |
|----------|--------------------|
| WhatsApp | @whatsappbot:rawgrowth.local |
| Instagram/Facebook | @metabot:rawgrowth.local |
| Slack | @slackbot:rawgrowth.local |
| Discord | @discordbot:rawgrowth.local |
| Twitter | @twitterbot:rawgrowth.local |
Bridge Status Check
cd /Users/scanbot/BusinessOS/apps/comms-agent && docker compose ps
If any bridge is down:
cd /Users/scanbot/BusinessOS/apps/comms-agent && docker compose restart bridge-whatsapp
WhatsApp reconnect (if TRANSIENT_DISCONNECT):
The bridge auto-reconnects. Check status with list-logins in the management room.