Building a Scalable Message Monitor and Auto-Response System for GetMyBoat Leads
What We Built
We designed and began implementing a message monitoring and auto-acknowledgment system that integrates GetMyBoat lead notifications with a proposal-generation workflow. The system uses Playwright for browser automation to configure Google My Business (GMB) notification settings, combines polling-based lead detection with template-driven proposal drafting, and maintains a human-in-the-loop approval gate before any outbound communication.
Architecture Overview
The solution spans four distinct layers:
- Detection Layer: Playwright-based browser automation monitoring GetMyBoat messaging and GMB settings
- Orchestration Layer: Lambda functions and local polling agents managing the workflow state machine
- Data Layer: DynamoDB tables storing proposals, charters, and crew roster; S3 for historical proposal templates
- Communication Layer: SMS via
send-smstool (iMessage bridge), email via SES, and the approval workflow gate
Technical Implementation Details
Playwright Configuration for GMB Settings
The first phase focused on automating the Google My Business notification routing. Rather than manually configuring each notification channel, we use Playwright to:
# Pseudocode for GMB notification automation
const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();
// Navigate to GMB settings (uses credentials from creds.txt labels)
await page.goto('https://business.google.com/settings');
await page.fill('input[name="email"]', gmb_email_label);
// MFA and password handling via secure credential store
await authenticateGMB(page, creds_store);
// Locate notification preferences panel
const notificationSection = await page.locator('[data-section="notifications"]');
await notificationSection.click();
// Configure routing: new messages → jadasailing@gmail.com
const emailInput = await page.locator('input[placeholder*="notification email"]');
await emailInput.fill('jadasailing@gmail.com');
await page.locator('button:has-text("Save")').click();
Why Playwright over UI-based tools: Playwright gives us versioned, repeatable automation that survives Google UI changes better than click-based RPA. It also integrates directly into our Node.js Lambda environment without external dependencies.
Message Monitor Polling Loop
The core of the system is a polling agent that checks for new GetMyBoat messages and fires acknowledgments. The workflow is:
- Poll GetMyBoat inbox every 2 minutes (configurable via environment)
- On new lead message, extract sender details and boat inquiry metadata
- Draft an immediate acknowledgment SMS using a template
- Queue a proposal-generation task for async processing
- Wait for human approval before sending either message
The polling loop lives in ~/jada-ops/crew-monitor/index.js (local development) and will eventually run as a scheduled CloudWatch Rule triggering a Lambda every 2 minutes during business hours (08:00–17:00 UTC-8).
// Simplified polling loop structure
async function pollForNewLeads() {
const lastCheckTime = await getLastPollTimestamp('getmyboat-last-check');
const inbox = await fetchGetMyBoatInbox(lastCheckTime);
for (const message of inbox.unreadMessages) {
const leadProfile = {
senderName: message.from.name,
boatModel: message.metadata.boat,
requestType: message.metadata.inquiryType,
timestamp: message.createdAt
};
// Draft immediate ack
const ackText = await draftAckMessage(leadProfile);
// Queue proposal generation async
await sqs.sendMessage({
QueueUrl: process.env.PROPOSAL_QUEUE_URL,
MessageBody: JSON.stringify({
leadId: message.id,
profile: leadProfile,
precedent_proposals: await getPrecedentProposals()
})
});
// Return ack for human approval (don't send yet)
await cacheForApproval('ack_message', {
leadId: message.id,
text: ackText,
status: 'pending_approval'
});
}
await updateLastPollTimestamp('getmyboat-last-check', new Date());
}
Proposal Synthesis from Historical Templates
Rather than starting from scratch, the proposal generator reads three precedent proposals (Dylan, Noelle, Molly charters from ~/jada-ops/proposals/) and synthesizes a new one tailored to the current lead. This uses:
- S3 bucket:
jada-heritage-research(stores historical proposal markdown, pricing tiers, charter terms) - DynamoDB table:
jada-charters(indexed by skipper, includes revenue, dates, passenger count, boat specs) - DynamoDB table:
jada-roster(crew availability, certifications, previous trips with leads)
The Lambda GenerateProposalFromLead (triggered by SQS message):
- Fetches the three precedent proposals from S3
- Extracts pricing logic, terms, and structure via regex and semantic parsing
- Queries
jada-chartersfor similar historical charters (boat type, duration, passenger count) - Generates a markdown proposal matching the tone and format of precedents
- Stores result in DynamoDB
jada-proposalswithstatus: 'draft' - Publishes SNS notification so the dashboard can prompt for approval
Human-in-the-Loop Approval Gate
This is intentional friction. No SMS or email leaves without your explicit one-tap approval. Two approval modes:
- Attended (recommended for now): Each morning after 08:00, you return to the session and say "send Travis confirmation" or "approve lead proposal." We cache the data in memory (charter details, draft text) and you tap to send.
- Unattended scheduled: Lambda fires automatically and sends SMS/email. This trades speed for risk — overnight auto-sends without human review are exactly the kind of hard-to-reverse outbound action we should avoid.
For a single confirmation or proposal that can wait until morning, attended is strictly better: same UX speed (one tap), zero risk of a wrong message going to a real person while you're not looking.
Infrastructure and Data Flow
- GMB credentials: Stored in
creds.txtwith masked labels; accessed only by Playwright context during automation - GetMyBoat API: No direct API; polling via Playwright login (credentials from creds.txt) to avoid third-party token management
- SMS delivery:
send-smsexecutable (iMessage bridge); respects quiet hours (17:00–08:00 UTC-8) to avoid disturbing crew