Introduction

When I started Dream Futures Tech in Pompano Beach, I quickly realized that most small trade businesses in Palm Beach County—plumbers, HVAC technicians, roofers—were missing two things: a simple online way for prospects to request a quote, and a reliable system to keep those leads warm. I built a lightweight website for my own shop and paired it with a home‑hosted automation server that uses Node.js, a tiny SQLite database, and the OpenAI API to send personalized follow‑up emails. Below is a step‑by‑step walk‑through of the pieces I put together, so you can replicate the same workflow for any local service business.

The Business Need

  • Capture leads: A single‑page form that works on mobile and desktop.
  • Local SEO: Structured data that tells Google the business is a plumber in Pompano Beach.
  • Automated nurture: An email that references the specific service the visitor asked about, written by an AI model.
  • Low cost: All components run on a Raspberry Pi‑class machine at home, using free tiers of OpenAI and a modest email‑sending service.

Building the Landing Page

I kept the front‑end to plain HTML/CSS so it loads instantly on any device. The form posts to an Express endpoint that stores the data.

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Fast Quote – Pompano Beach Plumbing</title> <link rel="stylesheet" href="/styles.css" /> </head> <body> <h1>Get a Free Quote Today</h1> <form id="leadForm" method="POST" action="/api/lead"> <label>Name <input type="text" name="name" required /> </label> <label>Email <input type="email" name="email" required /> </label> <label>Phone <input type="tel" name="phone" /> </label> <label>Service Needed <select name="service" required> <option value="leak repair">Leak Repair</option> <option value="water heater install">Water Heater Install</option> <option value="drain cleaning">Drain Cleaning</option> </select> </label> <button type="submit">Request Quote</button> </form> </body> </html> 

The page lives in public/ and is served statically by Express:

const express = require('express'); const app = express(); app.use(express.static('public')); app.use(express.json()); app.use(express.urlencoded({ extended: true })); 

Adding Structured Data for Local SEO

Google likes JSON‑LD that describes the business. I embed it directly in the HTML head. This is a judgment call: the extra markup costs a few bytes but improves local search visibility.

<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Plumber", "name": "Fast Quote Plumbing", "url": "https://fastquotepompano.com", "telephone": "+1-954-555-0123", "address": { "@type": "PostalAddress", "streetAddress": "123 Main St", "addressLocality": "Pompano Beach", "addressRegion": "FL", "postalCode": "33060", "addressCountry": "US" }, "geo": { "@type": "GeoCoordinates", "latitude": 26.2379, "longitude": -80.1248 }, "areaServed": "Palm Beach County", "priceRange": "$$" } </script> 

Setting Up the Automation Server

All back‑end logic lives on a small Ubuntu VM at home. I chose SQLite because it requires no separate service and is fast enough for a few dozen leads per day.

const sqlite3 = require('sqlite3').verbose(); const db = new sqlite3.Database('./leads.db'); db.run(`CREATE TABLE IF NOT EXISTS leads ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT, phone TEXT, service TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, emailed INTEGER DEFAULT 0 )`); 

The POST route validates the payload and inserts a row:

app.post('/api/lead', (req, res) => { const { name, email, phone, service } = req.body; if (!name || !email || !service) { return res.status(400).json({ error: 'Missing required fields' }); } const stmt = db.prepare('INSERT INTO leads (name,email,phone,service) VALUES (?,?,?,?)'); stmt.run(name, email, phone || '', service, function(err) { if (err) return res.status(500).json({ error: 'Database error' }); res.json({ success: true, id: this.lastID }); }); }); 

AI‑Powered Follow‑Up Emails

Instead of a generic “Thanks for contacting us,” I let GPT‑4 draft a short, service‑specific email. The prompt is deliberately simple; more elaborate prompts increase token usage without measurable benefit for this use case.

const { Configuration, OpenAIApi } = require('openai'); const config = new Configuration({ apiKey: process.env.OPENAI_API_KEY }); const openai = new OpenAIApi(config); async function generateEmail(lead) { const prompt = `Write a friendly email to a homeowner in Pompano Beach who just requested a quote for ${lead.service}. Mention our company name Fast Quote Plumbing, include a phone number, and ask them to schedule a time. Keep it under 150 words.`; const response = await openai.createChatCompletion({ model: 'gpt-4o-mini', messages: [{ role: 'user', content: prompt }], temperature: 0.6, max_tokens: 200, }); return response.data.choices[0].message.content.trim(); } 

I use nodemailer with a free SMTP relay (Mailgun’s sandbox domain) to send the email. The code runs in a background job that checks for unsent leads every five minutes.

const nodemailer = require('nodemailer'); const transporter = nodemailer.createTransport({ host: 'smtp.mailgun.org', port: 587, auth: { user: process.env.MG_USER, pass: process.env.MG_PASS, }, }); function sendEmail(to, subject, html) { return transporter.sendMail({ from: 'Fast Quote Plumbing <quotes@fastquotepompano.com>', to, subject, html, }); } function processQueue() { db.all('SELECT * FROM leads WHERE emailed = 0', async (err, rows) => { if (err) return console.error(err); for (const lead of rows) { try { const body = await generateEmail(lead); await sendEmail(lead.email, 'Your Free Plumbing Quote', `<p>${body}</p>`); db.run('UPDATE leads SET emailed = 1 WHERE id = ?', lead.id); console.log(`Emailed lead #${lead.id}`); } catch (e) { console.error('Failed to email lead', lead.id, e); } } }); } setInterval(processQueue, 5 * 60 * 1000); // every 5 minutes 

Deploying and Testing

  1. Clone the repo on the home server.
  2. Create a .env file with OPENAI_API_KEY, MG_USER, and MG_PASS.
  3. Run npm install and then node index.js.
  4. Verify the form submits by checking the SQLite file (sqlite3 leads.db "SELECT * FROM leads;").
  5. Use a tool like MailHog locally to confirm the email payload before sending to real addresses.

I keep the server behind UFW with only ports 80/443 open, and I terminate the Node process with pm2 so it restarts automatically on power loss.

Lessons Learned

  • Keep the prompt short – the model produces good results with a single sentence describing the service.
  • Rate limits matter – the free OpenAI tier allows 3 requests per minute; batching leads into a single call would be needed for higher volume.
  • Local SEO wins – Adding the JSON‑LD snippet moved the business from page 3 to page 1 for “plumber Pompano Beach” in my own tests, even without backlinks.
  • Simplicity over perfection – A plain HTML form with server‑side validation beats a fancy React SPA for this audience; trade owners care about reliability, not flash.

Conclusion

By combining a minimal landing page, structured data for local SEO, and an AI‑driven email follow‑up, you can give a Palm Beach County trade business a professional online presence without hiring a full‑time marketer. The whole stack runs on inexpensive hardware you already have at home, and the code is small enough to understand and tweak as the business grows.

I build sites and automations for small businesses at dreamfuturestech.com.