Every contractor we've talked to has the same blind spot: they spend money on Google Ads + Yelp + Angi + GBP every month and have NO IDEA which one actually pays back. /portal/marketing fixes that with a single grid: Spend × Leads × Bookings × Revenue × ROI per source.
How to use it
- 1Add your sources
Go to Settings → Lead sources & ad spend (or click "Manage sources & spend" from the top of the ROI page; direct URL is /portal/marketing/sources). 8 channels are seeded by default: Google Ads, LSA, Facebook Ads, Yelp, Angi, GBP, Word of Mouth, Direct. Add new ones (Nextdoor, Thumbtack, mailers) as needed. Archive old ones — past attribution preserved.
- 2Enter your monthly spend
On the same page, click any month's cell next to a source and type the dollar amount. We store one row per (source, month) so YTD reports compute against actual month-by-month spend, not today's UI value.
- 3Read the grid
/portal/marketing shows Spend / Leads / Bookings / Revenue / ROI per source for the period (this month / last month / 90d / YTD). ROI is color-coded: green for ≥5x, neutral for 2-5x, red below 1x.
How attribution works
When a lead comes in, we tag the row with the lead_source_id matching whatever channel it came from (UTM params for web, Hannah's "how'd you find us?" for calls, Direct for the booking widget). Bookings count by their tagged source. Revenue is FIRST-TOUCH: paid invoices credit the source on the customer's first attributed appointment — so a returning customer doesn't double-count under their original source.
What the warning band means
If a source has spend > $0 but zero bookings in the period, /portal/marketing surfaces a red banner at the top — "money out, nothing booked. Worth investigating before next month's budget runs." That's usually the contractor's #1 "oh, we should turn THAT off" moment.
| ROI ≥ 5x | Pour more in. This channel is printing money — test 2x your monthly budget next cycle and see if the math holds. |
| ROI 2-5x | Healthy. Most paid ad channels live here. Keep it where it is unless you have a specific reason to move. |
| ROI 1-2x | Marginal. Costs are eating most of the revenue. Optimize ad copy + audience targeting before pulling the budget. |
| ROI < 1x | Losing money. Pull the spend, run a 30-day "organic only" experiment, and re-decide whether to come back to it. |
Brand-new clients get the 8 default sources auto-seeded the moment they finish onboarding. You don't have to import anything; you just add your monthly spend numbers and the grid lights up.
Today only widget bookings + UTM-tagged form submissions auto-tag a source. Voice calls have a `source` text column populated by the AI but we don't auto-resolve to a lead_source_id yet. The next pass will let Hannah ask "how'd you find us?" and map the answer to a source — until then, you can manually tag voice calls from /portal/calls if it matters.