Nir Levy

Your water bill doubled. The utility's website shows you a PDF and a phone number. That's the whole support experience for most of North America.

WaterShortcut is a small tool that answers one question — why did this bill change? — and ranks what to do about it. Upload the bill or type in four numbers, and you get a plain-English diagnosis: usage really did go up, the rate changed, sewer is being billed off a winter average, or something is leaking. Every claim links to the source you can verify yourself.

Here is how it's built, including the number that says it isn't working yet.

The stack

The whole thing is one Cloudflare Worker.

  • Hono for routing, serving both the marketing surface and the JSON API from the same Worker
  • React 19 + Vite for the interactive tools, compiled through the Cloudflare Vite plugin
  • D1 for persistence, KV for sessions and growth counters
  • Google Document AI for bill extraction, then a model pass to turn extracted line items into a ranked explanation
  • Playwright + Vitest in CI, plus a Worker smoke check that asserts analysis.topMoves actually comes back from /api/analyze-bill in production, not just locally

No origin server, no container, no cold-start story to manage. Deploys are wrangler deploy and take about twenty seconds.

Service bindings are the actual architecture

This is the part worth stealing.

WaterShortcut is one of a large portfolio of niche domains I run, and they all need the same three things: an AI call path, billing, and growth instrumentation. Copying that into every project is how you end up with a dozen slightly-different Stripe integrations.

Instead each site is a thin Worker that declares service bindings to shared Workers:

"services": [
  { "binding": "PORTFOLIO_AI_SERVICE",      "entrypoint": "PortfolioAIService" },
  { "binding": "PORTFOLIO_BILLING_SERVICE", "entrypoint": "PortfolioBillingService" },
  { "binding": "PORTFOLIO_GROWTH_SERVICE",  "entrypoint": "PortfolioGrowthService" }
]

Enter fullscreen mode Exit fullscreen mode

Calls to those bindings are RPC over Cloudflare's network, not public HTTP. No API keys to rotate per site, no egress, no auth handshake, and typed entrypoints so a breaking change in the shared service fails at tsc in every consumer instead of at 2am in production. One shared D1 instance means a user who shows up on a second domain is already known.

The cost of this pattern is real: a bug in the shared billing Worker is a bug on every site at once. That's why the shared services get the strict test coverage and the leaf sites get to stay disposable.

Prerendering, because a SPA that doesn't rank is a hobby

An interactive diagnosis tool wants to be a SPA. Organic search wants HTML. The build script does both — vite build, then a prerender pass that walks every public route and writes real markup, then a critical-CSS inline step. Search engines and AI crawlers get complete documents; the React app hydrates over them for anyone who starts clicking.

Along the way we deleted a lot. The rotating hero carousel went. So did the vague "next right step" copy and a set of SEO stub pages that were in the sitemap and returned 404. Every remaining tool has its own landing page with a live check that it actually returns 200.

Three rules we shipped under

  1. The core path is free and needs no account. You can diagnose a bill without giving us anything. Credits exist for heavier analysis, not as a toll on the basic answer.
  2. No dark patterns. Ad slots have reserved layout so they never push content around, and they never sit above the primary action.
  3. Delete promises we can't keep. Everything is labelled an educational estimate. We don't sell bill data. We say out loud that we're not a plumber, and for an active leak the right answer is a licensed one, not a web app.

The honest number

Our automated quality gate checks page views, CTA clicks, and whether the four key routes return 200. Latest run:

page_views: 207
cta_clicks: 0
signal:     traffic_with_delight_gap

Enter fullscreen mode Exit fullscreen mode

Two hundred people arrived and not one clicked the thing the entire site exists to do.

That is not a traffic problem, and no amount of additional SEO fixes it. It's a promise problem: the landing page apparently explains what the tool is without making anyone believe it will help them, right now, with the bill sitting in front of them. A tool that requires you to first go find a PDF has already asked for too much.

So the next iteration inverts it. The four numbers that drive the diagnosis — current bill, previous bill, gallons, billing days — go on the landing page itself as the first thing you see, with a demo bill prefilled for anyone who wants to poke at it first. The upload flow becomes the power-user path rather than the front door. Same engine, one less step before the payoff.

I'd rather publish the zero than wait until it's a nicer number.

Takeaways

  • Service bindings turn a portfolio of small sites into one platform with many faces, and the typed RPC catches integration breaks at build time.
  • Prerender your SPA if organic search matters at all. It's a build step, not a rewrite.
  • Instrument the click you actually care about, then look at it. "Traffic is up" hid a conversion rate of exactly zero.
  • Publishing the failure number is cheaper than defending a nicer one.

Try it on a real bill: https://watershortcut.com/analyze-water-bill

Happy to answer architecture questions in the comments — especially about the shared-service pattern, which I've now run across enough sites to have opinions about.