Silent failure detection
Declined and expiring cards surfaced the day they fail rather than the quarter they compound — each one carrying the customer, the plan and the dollar value at risk.
MemberBay
MemberBay sits on top of ServiceTitan and watches the part of your recurring revenue that fails quietly. It tells you in March, not at renewal.
The problem
A maintenance membership does not end with a cancellation email. It ends with a card that expired in March, a tune-up nobody booked in June, and a renewal date that passed in September while the office was busy. By the time it surfaces in a report the customer is gone and the acquisition cost is already spent.
ServiceTitan holds every one of those signals. It just does not make them loud. Most contractors find out at renewal season, run a panic campaign, and win back a fraction. The gap between “the data exists” and “somebody acted on it” is where the money is.
What it does
Declined and expiring cards surfaced the day they fail rather than the quarter they compound — each one carrying the customer, the plan and the dollar value at risk.
Every membership ranked by expiry date and revenue at risk, so the follow-up list writes itself instead of being rebuilt by hand each month.
Members who paid for two visits and received one. The earliest churn signal in the business, and the one almost nobody watches.
An AI agent that answers questions about the book of business in plain language, a Slack bot that pushes what changed overnight, and a Chrome extension that puts the numbers inside ServiceTitan itself.
Under the hood
Tenant isolation on every row, an identity layer mapping ServiceTitan IDs to internal UUIDs, and a cached report tier so the dashboard never re-queries the API on load. FastAPI and Postgres behind, React and TypeScript in front, Stripe for billing, and a supervisor process that restarts what falls over.
# the query that runs every nightfor m in servicetitan.memberships(tenant): if m.card.declined or m.expires_within(days=30): yield Risk(m.customer, m.plan, m.value_at_risk)Built with
Next
KuJI