All RevOps

Lead-to-Revenue Loop

A weekly system for businesses with a front desk and a till, like dental practices and gyms. It follows every ad lead to the sale and sends the payment data back to Meta and Google.

The problem

A dental practice running Meta ads gets told its leads cost $9. The report never says which of those leads paid.

After the form fill, the lead waits for the front desk to reply, books or doesn't, shows up or doesn't, and pays at the till. Each step lives in a different system: the ad account, the CRM, the booking app and the POS.

So owners judge ads on cost per lead. A campaign can look cheap and still bring in very few people who buy.

And a lot of the money leaks out after the ad. A lead who waits 2 hours for a reply books far less often than one who hears back in 5 minutes.

What I built

One command that runs every Monday at 7am. For each business it:

  • Pulls leads and reply times from the CRM, sales from the till and spend from the ad accounts.
  • Matches each till sale to the lead it came from, by phone or email.
  • Measures reply time, booking rate, show rate and ad spend per paying customer.
  • Runs 5 rules that look for leaking revenue.
  • Has Claude write the weekly note in the business's voice, then checks every number in it.
  • Emails the owner a report with a call-back list, and sends the week's buyers back to Meta and Google.

Matching a sale to its lead

Till data is messy. The same person is (512) 200-0142 in the CRM and +15122000142 at the till, or MAYA@GMAIL.COM instead of maya@gmail.com.

So the code cuts phone numbers to 10 digits and lower-cases emails. Then it credits each sale to the most recent lead from that person in the 45 days before it.

Some sales never match (someone pays on their partner's card, or a regular walks in). Those get reported as walk-ins and repeat visits, and they don't count as ad revenue.

Rules find the leaks

5 rules run on the numbers before Claude sees anything:

  • Median reply time over 15 minutes, counting only leads that arrived during opening hours. A form sent at midnight doesn't count against the front desk.
  • More than 10% of the week's leads never got a reply.
  • Show rate down more than 15% on last week.
  • A campaign that's cheaper per lead than average but brings in under half the account's revenue per ad dollar.
  • Ad spend per paying customer over 125% of the owner's target.

The rule I had to rewrite

My first cheap-leads rule flagged dental implants, because only 7% of implant leads became patients.

But each implant patient pays about $2,400, so the campaign earned $2.63 for every $1 it spent. Now a campaign needs cheap leads and a weak return before it gets flagged.

The code checks Claude

Claude gets totals only. It never sees a lead's name, email or phone number, and a test makes sure of it.

Every leak has to show up in Claude's note exactly once, and every $ and % figure has to match a number the code calculated. If a check fails, Claude gets 1 retry with the errors spelled out.

What it caught

I tested it on 3 made-up businesses, each with 90 days of leads and till sales, and hid 1 problem in each. The system doesn't know where.

  • Halcyon Dental: the front desk got short-staffed. Median reply time went from 6 minutes to 2.1 hours, 27% of leads never heard back, and bookings fell 44%. That week's revenue still looked fine, because patients booked earlier were still paying, so I told Claude to say the drop lands over the next 2 weeks.
  • Ironside Strength (gym): Meta's free 7-day pass form cost $8.66 per lead, so it got 70% more budget. Only 1.2% of those leads joined, at $706 per member. Google search brought members in at $119.
  • Lumen Aesthetics (med spa): reminder texts stopped sending. The show rate fell from 87% to 50% in a week, and cost per new client went from $193 to $330 against a $200 target.

The call-back list

Every report ends with the leads worth chasing today: anyone from the week who never heard back, plus anyone from the last 3 days who got a reply but didn't book.

Each one comes with a ready-to-send text and a rough value. Halcyon's list had 57 leads, worth about $7,400 if they book at the practice's normal rate (that's an estimate, and probably a generous one).

Telling Meta and Google who paid

Ad platforms aim at whatever you report back to them, and by default that's a form fill.

So each week the matched sales go back as offline conversions: Meta Conversions API events with hashed email and phone, and a Google Ads import file keyed on the click ID. That should push bidding toward people who look like buyers. I haven't run it against a live ad account yet.

Where it stands

It's a working build on sample data. No business is using it yet.

The full run works end to end and 42 automated tests cover it. The Meta upload, Google Sheets and email are built, but none of them has touched a real account.

The CRM and till connections are stubs with the HubSpot and Square field mappings written down. And the demo notes so far come from a template fallback, since I ran it without an API key.

What's next

  • Connect one real CRM and till system, probably HubSpot and Square.
  • Add CallRail, since a lot of dental and med spa leads phone in.
  • An instant text reply for new leads, since slow replies were behind Halcyon's 44% drop in bookings.
  • A 4-week pilot with one practice, comparing bookings and cost per patient before and after.

Stack

Python, the Claude API (structured output), Pydantic, Jinja, Google Sheets, the Meta Conversions API, Google Ads offline imports, Resend or SMTP, and cron or n8n.

Hand-drawn diagram: ad accounts, a CRM and a till system feed a weekly engine that matches each sale to its lead, measures reply time, show rate and cost per paying customer, checks for leaks and has Claude write a fact-checked note. It sends a branded email with a call-back list, fills a Google Sheet, and passes matched sales back to Meta and Google as offline conversions.