Skip to content

Facebook Messenger

The Messenger channel connects your agent to a Facebook Page’s Messenger inbox. Meta POSTs each message to a webhook the adapter serves, and replies go out through the Send API. It is provided by the external @mono-agent/messenger-adapter package and loaded through channels.plugins[]. Coverage: config — see feature-registry row messenger.graph.

  1. In the Meta developer console create an app with the Messenger product, connect your Page, and generate a Page access token. Copy the App secret from the app’s basic settings. Choose any string as the verify token.
  2. Put the three values in the agent’s .env:
Terminal window
MONO_AGENT_MESSENGER_PAGE_ACCESS_TOKEN=...
MONO_AGENT_MESSENGER_APP_SECRET=...
MONO_AGENT_MESSENGER_VERIFY_TOKEN=...
  1. Declare the plugin and allow one or more users:
{
"channels": {
"plugins": [
{
"package": "@mono-agent/messenger-adapter",
"id": "messenger",
"config": {
"enabled": true,
"allowedUserIds": ["1234567890123456"],
"host": "0.0.0.0",
"port": 8650,
"allowNonLoopback": true
}
}
]
}
}
  1. Expose http://<host>:8650/messenger/webhook over HTTPS (a reverse proxy or a tunnel such as cloudflared) and register that public URL in the app’s Messenger Webhooks settings with the same verify token, subscribed to messages and messaging_postbacks. Meta calls GET with hub.challenge during registration; the adapter answers it once the agent is running.
KeyTypeDefaultPurpose
config.enabledbooleanfalseOpt-in switch. Off means the channel reports “disabled” (not “waiting”).
config.allowedUserIdsstring[][]Page-scoped user ids (PSIDs) allowed to talk to the agent.
config.allowAllUsersbooleanfalseWhen true, every user is allowed; the allowlist is ignored.
config.hoststring127.0.0.1Bind address for the webhook server.
config.portinteger8650Bind port.
config.webhookPathstring/messenger/webhookWebhook route. <path>/health answers liveness checks.
config.apiVersionstringv21.0Graph API version used for sends.
config.allowNonLoopbackbooleanfalseRequired to bind anything other than loopback.
config.proactiveMessagingTypeRESPONSE | UPDATE | MESSAGE_TAGRESPONSESend API messaging_type for proactive cron/webhook deliveries.
config.proactiveTagstringPolicy tag required with MESSAGE_TAG, e.g. CONFIRMED_EVENT_UPDATE.

Secrets are read only from MONO_AGENT_MESSENGER_PAGE_ACCESS_TOKEN, MONO_AGENT_MESSENGER_APP_SECRET, and MONO_AGENT_MESSENGER_VERIFY_TOKEN — they have no JSON key. Every other field also has a MONO_AGENT_MESSENGER_* env override (see Environment variables).

A PSID is specific to your Page. Temporarily set allowAllUsers: true, send the Page a message, read the id from the start log’s unauthorized/handled lines or the run artifacts, then move it into allowedUserIds and turn allowAllUsers off. An unlisted sender receives a one-line denial; their text never reaches the agent.

Conversations are keyed messenger:<psid>. Cron jobs and webhook endpoints deliver to one with notifyConversationId: "messenger:<psid>": the final answer is posted verbatim as plain text, split into 2,000-character messages, and recorded to that conversation’s history so a later reply resumes with it in context. See Delivery & send tools.

Meta only delivers ordinary messages within 24 hours of the user’s last message. A reminder that may fire outside that window needs proactiveMessagingType: "MESSAGE_TAG" and a policy-compliant proactiveTag.

  • Webhook POSTs are verified with X-Hub-Signature-256 over the raw body, acknowledged immediately, and processed afterwards; duplicate deliveries are dropped by message id.
  • Messages from one user run in order; up to four queue behind an active turn, then the user gets a short busy reply. /cancel aborts the active turn and retires anything already queued behind it, so a withdrawn prompt never answers later; a message sent after the cancel runs normally. /help and /start answer without a model call.
  • Images and PDF/text files are downloaded from Meta’s CDN and passed as attachments. Downloads are restricted to HTTPS on fbcdn.net / fbsbx.com; redirects are followed manually with every hop re-checked against that policy, hostnames must resolve entirely to public addresses, and the 20 MiB cap is enforced while streaming (an absent or dishonest Content-Length cannot exhaust memory). Audio, video, locations, and other files are described in the request text.
  • Replies are never silently duplicated: Meta’s Send API has no idempotency key, so a message POST whose outcome is unknown (timeout, transport failure, 5xx) is reported as an ambiguous delivery instead of being replayed. Only a 429, which the server refused before acting, is retried.
  • Replies are plain text: Markdown is flattened before sending.

The Telegram personal-assistant playbook translates directly: add the @mono-agent/messenger-adapter entry under channels.plugins[] and point cron notifyConversationId values at messenger:<psid>. See the Channels overview for the shared allowlist and status model.