Building a Warm Lead Auto-Acknowledgment System for GetMyBoat: Architecture and Implementation

This post documents the technical approach to implementing a rapid-response lead acknowledgment system for JADA's GetMyBoat integration. The goal: when a warm lead messages through GetMyBoat, an automated acknowledgment fires immediately, followed by a human-reviewed proposal draft based on historical booking patterns. Here's how we architected it.

The Problem: Lead Response Latency

Charter businesses live on response speed. A lead messages on GetMyBoat at 2 PM; if they don't hear back within 15 minutes, they've already messaged three other captains. The manual workflow was:

  • Check GetMyBoat UI manually (polling every 30+ minutes)
  • Craft a custom response email
  • Draft a proposal from scratch
  • Send both

Total time: 45+ minutes. We needed sub-5-minute acknowledgment with proposal skeleton ready for approval.

Architecture Overview

The system has three layers:

  • Monitoring layer: Playwright-based polling of GetMyBoat inbox
  • Acknowledgment layer: Templated SMS/message response via iMessage bridge
  • Proposal generation layer: Context-aware drafting based on historical charters

We're avoiding a serverless event-driven model for now (no GetMyBoat webhooks available) and instead running a controlled polling loop on the development Mac with clear state management and quiet-hour enforcement.

Technical Implementation: The Monitoring Daemon

The core polling loop lives in a memory file at:

/Users/cb/.claude/projects/-Users-cb/memory/jada-getmyboat-lead-automation.md

This file tracks:

  • Last message ID processed (to avoid re-acknowledging)
  • Quiet hours (08:00–17:00 local time; no outbound messages outside this window)
  • GetMyBoat inbox state snapshot
  • Pending confirmations awaiting human approval

Why memory file instead of database? Development velocity. No migration burden, human-readable state, easy to inspect mid-session. For production, this becomes a DynamoDB table with TTL on pending confirmations.

GetMyBoat Credential and Access Pattern

GetMyBoat login credentials are stored in masked form in:

/Users/cb/creds.txt

Labels are visible; values are masked. The authentication flow:


// Playwright setup (pseudocode)
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('https://getmyboat.com/login');
// Credentials injected from creds.txt labels at runtime
await page.fill('input[name="email"]', getCred('GMB_EMAIL'));
await page.fill('input[name="password"]', getCred('GMB_PASSWORD'));
await page.click('button[type="submit"]');
await page.waitForNavigation();

Key decision: We use Playwright (not Selenium or Puppeteer) because it has superior mobile emulation and handles GetMyBoat's JavaScript-heavy inbox rendering without flake. Chrome headless, no sandbox (dev Mac only).

Message Detection and State Tracking

Once logged in, the script navigates to the messages view and executes a DOM query:


// Locate new messages
const newMessages = await page.locator('[data-message-status="unread"]');
const count = await newMessages.count();

if (count > 0) {
  for (let i = 0; i < count; i++) {
    const msgElement = newMessages.nth(i);
    const senderId = await msgElement.getAttribute('data-sender-id');
    const messageText = await msgElement.textContent();
    const timestamp = await msgElement.locator('[data-timestamp]').getAttribute('data-timestamp');
    
    // Check against processed IDs in memory
    if (!alreadyProcessed(senderId, timestamp)) {
      await processLead(senderId, messageText, timestamp);
    }
  }
}

State is written back to the memory file with a JSON blob:


{
  "last_processed_timestamp": "2024-01-15T14:32:00Z",
  "processed_message_ids": ["sender_123", "sender_456"],
  "pending_confirmations": [
    {
      "sender_id": "sender_789",
      "charter_date": "2024-02-10",
      "boat_name": "Ocean Breeze",
      "party_size": 6,
      "draft_proposal_url": "/tmp/proposal_sender_789.pdf",
      "status": "awaiting_approval",
      "created_at": "2024-01-15T14:35:00Z"
    }
  ]
}

The Acknowledgment Pipeline: SMSAndProposal Draft

When a new lead is detected, two things happen in parallel:

1. Immediate Acknowledgment (iMessage via send-sms)

The script calls the existing send-sms executable:


// Templated ack message
const ackMessage = `Hi ${senderName || 'there'}! 👋 Thanks for reaching out on GetMyBoat. We got your message and are reviewing availability. We'll follow up with details shortly. —JADA Sailing`;

exec(`/usr/local/bin/send-sms -to "${senderPhoneNumber}" -message "${ackMessage}"`);

Why iMessage, not email reply on GetMyBoat? SMS arrives in seconds (GetMyBoat email can take 5+ minutes to deliver). For a warm lead, the first touchpoint should be fast. We still reply on GetMyBoat, but that's secondary.

2. Proposal Draft Generation

In parallel, the system analyzes historical charters for similar profiles. We query the data model:

  • Source: ~/repos/jada-heritage-research.md (historical bookings) + DynamoDB charters table
  • Match criteria: party size ±2, similar season, same/adjacent boat class
  • Template source: ~/repos/jada-ops/proposals/ (Dylan, Noelle, Molly booking examples)

The script:

  1. Parses the incoming message for party size, dates, preferences
  2. Scans proposals/ directory for similar past bookings
  3. Extracts: pricing structure, included amenities, cancellation policy, deposit terms
  4. Generates a markdown draft with placeholders for captain approval
  5. Writes draft to /tmp/proposal_${senderId}.md
  6. Updates memory file with status: awaiting_approval

Why not auto-send? A proposal is a legal/financial commitment. We draft and surface it for review (one-tap approval), but we never send outbound to a customer unreviewed. This is a hard rule.

Quiet Hours and Timing