6.9 KiB
X Chat (encrypted X DMs)
X Chat is X's end-to-end encrypted direct-message system. The Hermes adapter connects your agent to a bot X account's DMs: message bodies are encrypted and decrypted locally with the official Chat XDK — X only ever routes ciphertext, and every message is signed so recipients can verify the sender.
Run
hermes xchat setupfor a guided walk-through, or pick X Chat inhermes gateway setup.
Prerequisites
- An X developer account with an app configured for OAuth 2.0 user context (Developer Console). X Chat endpoints require API access on your developer plan.
- A user access token for the bot account with scopes:
dm.read,dm.write,users.read,tweet.read(addoffline.accessto receive a refresh token so Hermes can auto-renew the ~2-hour access token, andmedia.writeto send encrypted file/image attachments). - Python 3.10+ (the
chatxdkE2EE binding is lazy-installed at first use).
Setup
hermes xchat setup
The wizard:
- Stores the OAuth2 access token (and optional refresh token + client id) in
~/.hermes/.env. - Derives the bot account's numeric user id via
GET /2/users/me. - Generates the E2EE identity + signing keypairs with the Chat XDK, saves the private-key blob to
~/.hermes/xchat/private_keys.b64(mode 600), and registers the public keys with the X API.
Key registration is rate limited to a few writes per 24 hours per account. The setup is resume-safe: the key blob and registration payload are persisted before any network call, so an interrupted or rate-limited run resumes the same identity instead of minting a new one.
Check state anytime:
hermes xchat status
Environment variables
| Variable | Required | Description |
|---|---|---|
XCHAT_ACCESS_TOKEN |
Yes | OAuth2 user access token (dm.read, dm.write, users.read, tweet.read) |
XCHAT_REFRESH_TOKEN |
Recommended | Refresh token — enables automatic access-token renewal (rotated on every refresh and re-persisted) |
XCHAT_CLIENT_ID |
With refresh | X app OAuth2 client id (required for token refresh) |
XCHAT_CLIENT_SECRET |
Optional | Only for confidential clients |
XCHAT_USER_ID |
Auto | Bot account's numeric user id (derived by setup) |
XCHAT_SIGNING_KEY_VERSION |
Auto | Registered public-key version (written by setup) |
XCHAT_PRIVATE_KEYS_B64 |
Optional | Key blob override — takes precedence over the blob file. ⚠️ This is the bot's entire E2EE identity: there is no forward secrecy in this protocol, so anyone holding the blob can decrypt all past and future messages. Keep it in ~/.hermes/.env (never commit it, never pass it on a command line) |
XCHAT_ALLOWED_USERS |
Recommended | Comma-separated numeric X user ids allowed to talk to the bot |
XCHAT_ALLOW_ALL_USERS |
Optional | true allows every sender (dev only) |
XCHAT_CONVERSATION_IDS |
Optional | Pin specific conversation ids to poll; omit to auto-discover |
XCHAT_POLL_INTERVAL |
Optional | Seconds between event polls (default 10, floor 2) |
XCHAT_SEND_READ_RECEIPTS |
Optional | true sends read receipts for processed messages (default false) |
XCHAT_REQUIRE_MENTION |
Optional | In group conversations, only respond when a wake word matches (default false) |
XCHAT_MENTION_PATTERNS |
Optional | Custom wake-word regexes (JSON list or comma-separated) |
XCHAT_HOME_CHANNEL |
Optional | Default conversation/user id for cron delivery |
XCHAT_HOME_CHANNEL_NAME |
Optional | Human label for the home channel |
How it works
- Inbound — the adapter polls each conversation's events endpoint, keeping a persistent per-conversation cursor (
~/.hermes/xchat/cursors.json) of the last processed event. On the very first sight of a conversation it batch-decrypts the backlog (decrypt_events) to seed the SDK's verified conversation-key cache without replying to old messages; after that every new event (including bursts larger than one page, and messages that arrived while the gateway was down) is processed exactly once.KeyChangeevents (conversation-key rotations) are verified and folded into the key cache automatically; message edits are treated as new messages. - Outbound — replies are encrypted and signed locally (
encrypt_messagewith the session identity), then POSTed as ciphertext. Replies to a specific message use the native threaded-reply event when the target is in the adapter's decrypted-event cache. - Media — both directions are fully encrypted. Inbound attachments are downloaded, decrypted with the conversation key for the event's key version, and cached locally so vision/file tools can read them. Outbound
MEDIA:<path>files (images, voice notes, videos, documents) are encrypted with the latest conversation key, uploaded through the 3-step chat-media flow, and attached bymedia_hash_key(requires themedia.writescope). - Senders — each new sender's public keys are fetched once and pushed into the XDK's signing-key store so their message signatures verify.
- New conversations — standalone sends (
hermes send xchat:<user-id>, cron delivery) to a bare numeric user id perform the conversation-key handshake automatically: both parties' key bindings are verified, a fresh conversation key is wrapped for each participant, and the key change is POSTed before the first message. - Identity — user ids are numeric X user ids; conversation ids look like
123-456(1:1) org123…(group).
Authorization
By default all senders are denied. Either:
- Set
XCHAT_ALLOWED_USERSto a comma-separated list of numeric X user ids, or - Use DM pairing — an unknown sender gets a pairing code; approve with
hermes pairing approve xchat <CODE>.
Group conversations
Group chats (g… conversation ids) work out of the box. To keep the bot quiet unless addressed:
XCHAT_REQUIRE_MENTION=true
The default wake words are hermes / hermes agent; override with XCHAT_MENTION_PATTERNS. DMs are never gated.
Using X Chat with cron jobs
cronjob(
action="create",
schedule="every 1h",
deliver="xchat", # uses XCHAT_HOME_CHANNEL
prompt="Check for alerts and summarise."
)
Or target a conversation directly:
hermes send xchat:<conversation-id> "Done!"
Standalone delivery opens an ephemeral E2EE session, seeds the conversation key from the conversation's backlog, encrypts, and sends — no running gateway required.
Limitations
- Polling latency. Inbound uses REST polling (default 10s). Webhook / activity-stream delivery may come later.
- Group creation. The bot participates in existing group conversations but does not create new groups or manage membership.
- Access tier. X Chat API availability depends on your X developer plan; media upload additionally requires the
media.writescope.