Forward messages to your WhatsApp Message Yourself chat, discuss them with an LLM, and let it send replies from your own number — wrapped in a ban-safety envelope (reply-only, humanized pacing, circuit breaker, daily cap).
Status: runs as a single always-on worker (Docker container or plain Node process). No inbound ports — the only HTTP listener is a loopback webhook.
WhatsApp phone ──forward──▶ "Message Yourself" chat
│ wacli sync --follow --webhook (loopback)
▼
bot.js (127.0.0.1 only)
│ Azure OpenAI chat + tools
▼
reply in self-chat / send to any chat via wacli send
bot.jsspawnswacli sync --followwith a localhost webhook and supervises it (restart with backoff, store-lock discipline).- Only messages in your self-chat are processed. Bot replies are prefixed
🤖so they never loop back into the model. - The model gets four tools:
send_message,search_contacts,list_chats,read_messages. It only sends when you explicitly tell it to. - Messages arriving within
DEBOUNCE_MSare batched into one turn, so multi-message forwards are read together. - Forward-origin derivation: WhatsApp strips the source from forwards, but the local store has full history — the bot full-text-searches it to recover which chat a forward came from, so "reply to that" needs no follow-up question.
- A style primer sampled from your own sent messages keeps drafts in your voice.
- Conversation memory persists in
state.json(/resetin the chat clears it).
The bot can also run on its own WhatsApp number, so you DM it like a normal contact instead of using the self-chat. Both channels work in parallel.
- Get a real (non-VoIP) number and register it on WhatsApp (WhatsApp Business on your existing phone works — it runs alongside regular WhatsApp).
- Link it to wacli's second store:
wacli --store "$BOT_STORE" auth - Nothing to restart — the running bot checks the store every minute, auto-starts the channel, and messages you when live.
Only your JIDs (SELF_JID / SELF_LID) can talk to the bot number; everyone else is ignored and logged.
git clone https://github.com/BaseLayerAI/whatsapp-self-chat-agent
cd whatsapp-self-chat-agent
cp .env.example .env # fill in Azure creds + your JIDs
wacli auth # scan the QR from WhatsApp (Settings → Linked devices)
./run.sh # starts webhook server + sync + botRequires Node 22+ and the wacli binary on PATH.
- Forward any message(s), then: "draft a polite decline", "summarize", "translate".
- "Send that to Alex" / "reply to mom: on my way" → sends from your number.
/resetclears memory,/statusshows health + send safety,/helpshows help.- Tidy commands (delete-for-you — vanish from your phone, the server store keeps a copy):
/clearall,/clearlast N,/undo,/cleannow.
Once the bot completes an action for a thread (e.g. sends a message on your behalf) and isn't waiting on you, that exchange is deleted-for-you after the chat has been idle 15 min. Pure discussions and open questions are never auto-deleted. CLEANUP_SELF_CHAT=0 disables; CLEANUP_IDLE_MS tunes the window.
Hourly sweep for direct chats where the last message is inbound and older than 24h; the bot pings you with a drafted reply and asks whether to send it. Muted/archived chats, groups, and one-way notification numbers are skipped. Capped per sweep and per day.
Unofficial WhatsApp automation carries account-restriction risk. The research behind these choices is in docs/ban-research.md; the short version is that recipient consent and account behavior dominate, so the design is conservative by construction:
- Reply-only. Never initiates to non-contacts; no automated hello on link. Only whitelisted owner JIDs get replies.
- Companion-device linking (QR) with a real SIM, passive sync — the lowest-risk integration path.
- Humanized pacing: all outbound serialized, 5–8 s randomized gap, typing indicator sized to the message for bot-number/third-party sends.
- Circuit breaker: any send error that looks like WhatsApp pushback (rate limit / restricted / forbidden / spam) pauses all sending for 1 h; 5 consecutive failures of any kind pause 10 min.
- Daily cap of 200 sends as a runaway-loop backstop.
Check the live safety state any time with /status.
| Variable | Required | Description |
|---|---|---|
AZURE_OPENAI_ENDPOINT |
yes | Azure OpenAI resource endpoint |
AZURE_OPENAI_KEY |
yes | Azure OpenAI API key |
AZURE_OPENAI_DEPLOYMENT |
yes | Chat model deployment name |
SELF_JID |
yes | Your phone JID ([email protected]) |
SELF_LID |
no | Your account's LID (alternate identifier) |
OWNER_NAME |
no | How the model refers to you (default the owner) |
SELF_STORE |
no | wacli store for the personal account (default ~/.wacli) |
BOT_STORE |
no | wacli store for the bot account (default ~/.wacli-bot) |
BOT_NUMBER |
no | Bot account's phone number (confirmed from the store at startup) |
WEBHOOK_PORT |
no | Loopback webhook port (default 8790) |
DEBOUNCE_MS |
no | Message batching window (default 3000) |
STATE_DIR / LOG_DIR |
no | Where state.json and logs live (server: a volume) |
CLEANUP_SELF_CHAT / CLEANUP_IDLE_MS / CLEANUP_SWEEP_MS |
no | Auto-cleanup knobs |
NUDGE_* |
no | Unanswered-message nudge knobs (see .env.example) |
The Dockerfile builds a self-contained image (Node base + wacli release binary). Mount a persistent volume and point the stores at it so linked-device sessions survive redeploys:
SELF_STORE=/data/wacli
BOT_STORE=/data/wacli-bot
STATE_DIR=/data
railway.toml is included: Dockerfile build, always-restart, one replica. The worker needs no public ingress. To authenticate on a server, run wacli auth against the volume-backed store (or link locally and copy the store to the volume once).
A linked-device session must only run in one place — running a second wacli sync against a copy of the same store connects the session twice and can get the account flagged.
cat > ~/Library/LaunchAgents/com.baselayer.whatsapp-agent.plist <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.baselayer.whatsapp-agent</string>
<key>ProgramArguments</key><array>
<string>/bin/bash</string><string>$HOME/whatsapp-self-chat-agent/run.sh</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>EnvironmentVariables</key><dict>
<key>PATH</key><string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>
<key>StandardOutPath</key><string>$HOME/whatsapp-self-chat-agent/logs/launchd.log</string>
<key>StandardErrorPath</key><string>$HOME/whatsapp-self-chat-agent/logs/launchd.log</string>
</dict></plist>
EOF
launchctl load ~/Library/LaunchAgents/com.baselayer.whatsapp-agent.plistOperational details (credentials, failure signatures, recovery) are in docs/RUNBOOK.md.
- WhatsApp unlinks companion devices after long inactivity; if sends fail with "not authenticated", run
wacli authagain. - Logs:
logs/bot.log(bot),logs/sync-self.log/logs/sync-bot.log(wacli sync).