Doc Status: Design target with current implementation notes. /llm/manychat already exists and sends asynchronous final answers through ManyChat sendContent. The natural typing-indicator worker is the next implementation step and depends on a confirmed ManyChat typing-indicator API surface.

Why This Matters

Guests who message House of Legends on Instagram, Messenger, or WhatsApp expect a human-paced chat experience. The integration must acknowledge ManyChat immediately, prepare the real answer in the background, and keep the conversation feeling alive while the AI agent checks live show, pricing, and availability data. ManyChat remains the social messaging layer. Mastra owns the AI turn, and Hono remains the source of truth for public catalog facts, availability, pricing, policies, booking links, and inquiry submission.

Target Experience

When ManyChat sends a guest message to House of Legends:
1

Receive the ManyChat request

ManyChat calls POST https://houseoflegends.vn/llm/manychat with the guest message, channel, locale, and contact data.
2

Acknowledge immediately

Mastra validates the shared webhook token, extracts the message and subscriber ID, then returns a fast ManyChat Dynamic Block response so ManyChat does not wait on the LLM.
3

Start the background worker

The Worker uses executionContext.waitUntil(...) to continue after the HTTP response has been returned to ManyChat.
4

Run two background tasks together

The background worker starts response generation and a typing-indicator loop at the same time.
5

Send the final answer

When generation completes, Mastra audits generated House of Legends links, sends the final answer through ManyChat sendContent, and always turns typing off in cleanup.

Runtime Flow

Current Implementation

The current code already implements the main async path:
ConcernCurrent behaviorFile
RoutePOST /llm/manychat mounted in the Mastra Workerapps/mastra/src/worker.ts
AuthRequires x-hol-manychat-token or Authorization: Bearer ...apps/mastra/src/manychat/manychat-route.ts
Request parsingAccepts last_input_text, text, message, query, or contact.last_input_textapps/mastra/src/manychat/manychat-route.ts
ChannelsAccepts facebook, messenger alias, instagram, whatsapp, and telegramapps/mastra/src/manychat/manychat-route.ts
Async ackReturns a ManyChat Dynamic Block version: "v2" acknowledgement when a subscriber ID is availableapps/mastra/src/manychat/manychat-route.ts
Final answerCalls the Mastra customer-support router, audits internal links, then calls ManyChat sendContentapps/mastra/src/manychat/manychat-route.ts
Data sourceUses @hol/client and Hono assistant-safe endpoints, not direct database accessapps/mastra/src/mastra/tools/catalog-tools.ts
The typing-indicator loop is not implemented yet. Do not describe production behavior as if typing already runs behind the scenes until the code ships and is verified in ManyChat.

Background Worker Contract

The ideal background worker should be shaped like this:
async function processAsyncManyChatAgentResponse(input: ProcessAsyncManyChatAgentResponseInput): Promise<void> {
  const typingController = new AbortController();
  const typingPromise = runManyChatTypingIndicatorLoop({
    env: input.env,
    channel: input.channel,
    subscriberId: input.subscriberId,
    signal: typingController.signal,
  });

  try {
    const assistantText = await createManyChatAgentText({
      env: input.env,
      channel: input.channel,
      locale: input.locale,
      text: input.text,
      pageUrl: input.pageUrl,
      isFirstMessage: input.isFirstMessage,
      includeExternalMessageCallback: false,
      agentResponseTimeoutMs: undefined,
    });

    await auditManyChatResponseLinks({
      env: input.env,
      channel: input.channel,
      locale: input.locale,
      text: assistantText,
      pageUrl: input.pageUrl,
      isFirstMessage: input.isFirstMessage,
      includeExternalMessageCallback: false,
    });

    await sendManyChatContent({
      env: input.env,
      channel: input.channel,
      locale: input.locale,
      text: assistantText,
      subscriberId: input.subscriberId,
    });
  } finally {
    typingController.abort();
    await stopManyChatTypingIndicator({
      env: input.env,
      channel: input.channel,
      subscriberId: input.subscriberId,
    });
    await typingPromise.catch(() => undefined);
  }
}
The exact implementation can differ, but the cleanup rule is not optional: typing must be turned off after generation succeeds, fails, times out, or final delivery fails.

Typing Indicator Behavior

The typing loop should feel natural without becoming noisy:
RuleRequirement
Start quicklyTurn typing on shortly after the ack, unless the final answer is already ready.
Random cadenceAlternate typing on and off with jittered delays so it does not look robotic.
Low call volumeKeep the interval conservative to avoid ManyChat Public API limits and channel throttling.
Final cleanupAlways send a final typing-off call in finally.
Failure isolationTyping API failures should be logged and ignored. They must never block response generation or final sendContent.
Channel awarenessOnly run typing where ManyChat confirms support for the channel.
Recommended starting values:
SettingValue
Initial delay400-900 ms
Typing-on window2.2-4.8 seconds
Typing-off pause600-1,800 ms
Maximum loop duration55 seconds
Maximum failures before silence3
These values should be constants in manychat-route.ts or a small ManyChat helper module, not scattered through the route.

ManyChat API Dependency

The current public ManyChat Swagger for the Page API exposes the sending namespace with:
  • POST /fb/sending/sendContent
  • POST /fb/sending/sendContentByUserRef
  • POST /fb/sending/sendFlow
Fern uses the public OpenAPI endpoint at https://api.manychat.com/swagger/compileJson?type=Page_API. The generation script runs fern api update into an ignored temporary path during generation; do not keep a checked-in local copy of the vendor spec. It does not show a public typing-indicator endpoint. Before implementing the loop, confirm one of these with the ManyChat account/API documentation:
ManyChat provides an official typing action endpoint. Implement a small typed helper such as sendManyChatTypingIndicator and keep the endpoint path/body in one place.
Do not call Meta Graph API directly from Mastra for these channels unless the ownership model changes. ManyChat owns platform delivery, opt-in state, channel limits, and handoff routing.

Request Contract

ManyChat should send:
{
  "channel": "instagram",
  "locale": "{{locale}}",
  "language": "{{language}}",
  "last_input_text": "{{last_input_text}}",
  "response_mode": "async",
  "is_first_message": false,
  "contact": {{full_contact_data}}
}
The async path needs a numeric subscriber ID from one of these places:
  • subscriber_id
  • id
  • user_id
  • contact.id
  • contact.user_id
If no subscriber ID is available, Mastra cannot send the final answer later through sendContent; the route falls back to synchronous generation.

Environment Variables

MANYCHAT_WEBHOOK_TOKEN=replace_with_shared_webhook_token
MANYCHAT_API_TOKEN=replace_with_manychat_public_api_token

Operational Rules

  • Do not hardcode show names, dates, prices, capacity, booking URLs, or policy in ManyChat flows.
  • Do not send secrets through ManyChat message text, custom fields, or public replies.
  • Do not send payment data, private reservation notes, or internal operator data to ManyChat.
  • Keep all House of Legends links absolute and locale-prefixed.
  • If the agent needs richer live facts, improve Hono assistant-safe endpoints first, then consume them through @hol/client.
  • Typing simulation must be best-effort only. A typing failure is a cosmetic failure, not a customer-support failure.

Verification

1

Verify the current route

pnpm --filter @hol/mastra lint
pnpm --filter @hol/mastra typecheck
pnpm --filter @hol/mastra build
2

Run a ManyChat external request test

Confirm the request returns HTTP 200 with version: "v2" and the immediate acknowledgement message.
3

Confirm async final delivery

Ask a live-data question such as “What shows are available this weekend?” and confirm the final answer arrives through ManyChat after the background agent turn finishes.
4

Verify typing once implemented

Confirm typing starts after the ack, toggles naturally while generation runs, and turns off after the final answer is sent. Repeat on every enabled channel.
5

Check failure cleanup

Force an agent error, ManyChat API error, and timeout. Confirm logs are useful and typing still turns off.

References