Messenger Intelligence System
AI-powered auto-responders for the ECTC, Increase Construction, and Australis Roofing Facebook Pages. Trained on 3,800+ real conversations. Responds in seconds, routes to forms, qualifies candidates, and escalates to humans when needed.
3,887
Conversations Indexed
52/52
Test Scenarios Passed
Live counts as of 12 Jun 2026 — ECTC 1,621 · IC 1,830 · Australis Roofing 436 conversations.
Architecture
The system lives inside social-ads-platform (port 3003). A single webhook endpoint receives events from all subscribed Facebook Pages. The handler identifies which page/channel the message belongs to, loads the correct system prompt, and processes it through a 15-step pipeline.
Facebook Ad
→
Applicant Messages
→
Meta Webhook
→
Caddy Proxy
→
social-ads :3003
→
Claude Sonnet 4.6
→
Reply via Messenger API
Three Separate Systems
ECTC, IC, and Australis Roofing are completely independent. Different prompts, different decision logic, different kill switches, different ad funnels. They share the same handler infrastructure but never cross-contaminate.
| Property | ECTC | IC | Australis Roofing |
| Channel | fb_ectc_page | fb_ic_page | fb_ar_page |
| Kill switch | MESSENGER_AUTO_REPLY_ENABLED | IC_MESSENGER_AUTO_REPLY_ENABLED | AR_MESSENGER_AUTO_REPLY_ENABLED |
| Voice | Sean (marketing manager) | Drew (strategic founder) | Australis Roofing recruiter |
| Primary funnel | TC recruitment (employment forms) | Contractor growth (/get-work, /grow) + recruitment (resume to drew@) | Metal roofer recruitment |
| Conversations | 1,621 (10,554 messages) | 1,830 (10,770 messages) | 436 (3,405 messages) |
| Escalation | Slack recruitment channel (tagged ECTC) | Slack recruitment channel (tagged IC) | Handoff to Peter (phone + email required) |
| Lead router | Yes (form → Messenger confirmation) | No (uses /get-work and resume email) | No |
Adset-Aware Routing (IC)
When someone messages from a Facebook ad, Meta sends an ad_id. The handler resolves this to the adset name via the Graph API (cached). The adset name drives the entire response strategy. New adsets are handled automatically at runtime.
| Adset Pattern | Funnel | Goal | Key Response |
| Get Work - [Location] | Contractor | /get-work form | "Our builders need quality [trade] contractors" |
| Get Work - Roofers [Location] | Contractor (roofing) | /get-work form | Already know the trade, ask crew size |
| Grow Without Chaos | Business growth | /grow form | "This is exactly who we want to talk to." Escalate if 10+ crew. |
| Site Supervisor - [Location] | Recruitment (FT) | Resume to drew@ | Qualify for insurance exp. No exp = polite decline. |
| Site Supervisor (GOTT) - [Location] | Recruitment (FT) | Resume to drew@ | GOTT-specific roles, same qualification |
| Estimators/Assessors - [Location] | Recruitment (FT) | Resume to drew@ | Assessment/insurance background required |
| Supervising But Going Nowhere | Recruitment (FT) | Resume to drew@ | Skip qualification, pitch the upgrade directly |
| Roofers Wanted | Ambiguous | Clarify first | "Running your own crew or looking for a full time role?" |
ECTC Decision Logic
| Action | When | Response |
| employment_form_qld | QLD applicant with contact details | QLD employment form link |
| employment_form_nsw | NSW applicant with contact details | NSW employment form link |
| ask_tc_licence | First contact, no details yet | "Could you shoot me your mobile and email?" |
| waiting_on_course | Doesn't have TC licence yet | "Reach out once you've got your ticket" |
| course_redirect | Asks about TC courses | Training page link |
| pay_rate | Asks about pay | "National award rate, approx $35-36 p/h" |
| form_completed | Filled out the form | "I'll get your info to the local team" |
| escalate | Anger, legal, complaints, 5+ msgs | Silent handoff, Slack alert |
| no_action | Spam, acks ("thanks", "ok") | No reply sent |
Lead router integration (ECTC only): When an applicant fills out the employment form, the lead router processes it and calls back to the Messenger system. If the applicant has an open conversation, the bot sends "thanks [name], I'll get your info over to the local area manager" and marks it completed.
IC Decision Logic
| Action | When | Response |
| Contractor Funnel (Get Work Ads) |
| ask_trade_info | Vague interest, no detail | "What trade, how big's your crew, room for more work?" |
| get_work_form | Good fit with trade + detail | /get-work link + "I'll text you when I see it" |
| clarify_intent | Unclear if contractor or job seeker | "Running your own crew or looking for full time?" |
| grow_form | Fully booked / wants to scale | /grow link + "good problem to have" |
| Recruitment Funnel (Supervisor/Assessor Ads, Full Time) |
| ask_experience | First contact on recruitment ad | "Have you had insurance construction experience?" |
| request_resume | Has insurance exp / good fit | "Shoot your resume to drew@... I'll take a look" |
| not_a_fit | No insurance exp / irrelevant background | "For these roles we need insurance construction experience" |
| Business Growth (Grow Ads) |
| grow_form | Business owner wants to scale | /grow link |
| escalate | 10+ crew (high-value lead) | Silent handoff with context for Drew |
| Shared |
| escalate | Wants call, pay discussion, angry, legal | Silent handoff, Slack notification |
| no_action | Spam, acks, "ok thanks" | No reply |
15-Step Handler Pipeline
Every inbound message passes through this pipeline sequentially. Early exits at each checkpoint prevent unnecessary processing.
1
Skip Echoes
Ignore messages sent by the page itself (prevents reply loops)
2
Build Referral + Resolve Adset
If from an ad, resolve ad_id to adset name, campaign name via Graph API. Cached in-memory.
3
Skip Ad Postbacks
Facebook already sends a welcome message for ad clicks. Don't duplicate it.
4
Dedup
UNIQUE on message ID. Failed/pending messages pass through for auto-retry.
5
Kill Switch
Per-channel check. When off, messages logged but no reply sent.
6
Page Token Lookup
Resolves the correct page access token from channels table.
7
Upsert Conversation
Create/update conversation record with state, metadata, counts.
8
Escalation Check
If previously escalated to a human, bot stays silent.
9
Concurrency Guard
Atomic claim prevents parallel LLM calls for the same person.
10
Profile Fetch
Gets applicant name from Graph API (first contact only, non-blocking).
11
On-Demand History Fetch
For unknown contacts, pulls conversation history from Graph API before LLM decides.
12
Lead Router Cross-Reference
ECTC only: checks lead router DB for prior form submissions by phone/email.
13
Attachment Handling
Images via vision. Stickers: no_action. Voice: "type that out". Files: "email it".
14
Build Context + Call Claude
Last 10 messages + metadata + adset context. Filters system messages. Resilient parser handles non-strict JSON.
15
Execute Action
Send reply, escalate, or stay silent. Update state. Process queued messages (depth-limited).
Safety & Resilience
| Layer | Mechanism | Purpose |
| Kill Switches | Per-channel env vars in Infisical | Instant disable without deployment |
| HMAC Verification | SHA-256 on every webhook | Prevents spoofed requests |
| Message Dedup | UNIQUE constraint on mid | No double-replies |
| Concurrency Guard | Atomic claim per PSID | Prevents parallel LLM races |
| Silent Handoff | Bot stops + Slack alert | Applicant doesn't know it was a bot |
| 24hr Window | Enforced on all outbound paths | Meta messaging policy compliance |
| Auto-Retry | 6-hourly cron sweep | Self-healing, no messages lost |
| Resilient Parser | Extracts JSON from LLM reasoning | Handles non-strict output |
| System Msg Filter | 3-tier: storage, context, prompt | Facebook internals never reach LLM |
| Recursion Limit | Depth 2 for queued messages | Prevents runaway LLM calls |
Hard Rules (Both Channels)
NEVER confirm work available in a specific location
NEVER promise start dates, shifts, or hours
NEVER discuss pay rates (escalate if pushed)
NEVER say "staffing company" or "labour hire"
NEVER use em dashes in generated content
NEVER ask for info already provided
NEVER send the same message twice
NEVER say IC "has work" (the builders have it)
Background Tasks (Every 6 Hours)
1
Follow-Up Nudge
Form sent 4+ hours ago, no reply, still within 24hr window. One nudge per conversation.
2
Message Retry
Sweeps failed/pending inbound messages. Reprocesses through full pipeline.
3
Stale Marking
24hr window expired conversations marked stale.
Environment
| Variable | Purpose |
| FB_APP_ID / FB_APP_SECRET | Meta App credentials + HMAC verification |
| FB_WEBHOOK_VERIFY_TOKEN | Webhook subscription verification |
| MESSENGER_AUTO_REPLY_ENABLED | ECTC kill switch |
| IC_MESSENGER_AUTO_REPLY_ENABLED | IC kill switch |
| OPENROUTER_API_KEY | Claude Sonnet 4.6 via OpenRouter |
| SLACK_BOT_TOKEN | Escalation notifications |
Key Files
| File | Purpose |
| lib/messenger/handler.js | 15-step pipeline, LLM calls, retry logic, stale check |
| lib/messenger/system-prompt.js | ECTC + IC prompts, channel registry |
| lib/messenger/meta-api.js | Graph API: send, profile, typing, ad context resolver |
| server.js | Webhook routes, admin routes, form callback |
Database
| Table | Purpose | Key Columns |
| messenger_conversations | Per-applicant state | psid (PK), channel_id, state, first_name, last_action, metadata (adset, referral, lead_router) |
| messenger_messages | Audit trail + dedup | mid (UNIQUE), direction, message_text, ai_action, processing_status |
| messenger_escalations | Escalation tracking | psid, reason, slack_ts, resolved |
Cost
~$5-15/month per channel via OpenRouter (Sonnet 4.6). Each message ~$0.002-0.005 in tokens. Infrastructure: $0 (existing VPS).
Monitoring
Live dashboard in the ECTC Leads → Messenger tab. Stats, searchable conversation table, chat-bubble viewer, escalate/reset actions. Proxied via /api/proxy/messenger/*.