Ominvo beta launches July 23, 2026 — only 10 spots remain Join the waitlist →
All posts
Building OminvoDay 37

Day 37: Annual billing, Stripe prices, and the first feature page

June 9, 20265 min read

Two things shipped today that have been sitting on the list for a while: the annual billing toggle on the pricing page, and the first real destination for a Product dropdown item — /features/instant-alerts. There's also a Stripe housekeeping note and a process embarrassment worth documenting.

Annual billing toggle

The pricing page now defaults to Annual. That was a deliberate choice, not a default.

Most SaaS sites default to Monthly because it looks cheaper upfront — $30 feels less scary than $288. We went the other way. When a visitor lands on /pricing, the first number they see is $24/mo. That's the anchor. The Monthly option is there if they want it, but the framing is already set.

The 20% discount is the industry standard — Notion, Linear, Loom, all of them. "2 months free" is marketing math for 16.7%; we didn't bother. Twenty percent gives clean numbers: $288/yr for Chad (save $72), $948/yr for GigaChad (save $240). These are numbers you can explain to a bookkeeper without a calculator.

One thing worth noting for anyone building on Next.js: the getCTAHref and getCTALabel functions had to move from the server component into PricingClient. In the original architecture they lived in the page-level server component, which is fine until you need those functions to respond to client state — in this case, the toggle. Once the toggle becomes a React state variable, anything that reads it has to live on the client side too. It's a small refactor, but it's a good example of how the server/client boundary in Next.js forces you to actually think about where logic lives instead of just putting everything in the same file.

The hero badge was updated too: "Save 20% with annual billing — now available." Previously it said something vague. Now it says the number.

The FAQ got an explicit annual billing entry with concrete prices. If someone is about to put in a credit card for a year, they should be able to find exactly what they're paying without hunting.

Stripe price IDs

Annual prices are now created in Stripe test mode on the existing Chad and GigaChad products — no new products, just new prices attached to the existing ones. The environment now has four price ID variables:

  • STRIPE_CHAD_PRICE_ID
  • STRIPE_GIGACHAD_PRICE_ID
  • STRIPE_CHAD_ANNUAL_PRICE_ID
  • STRIPE_GIGACHAD_ANNUAL_PRICE_ID

The checkout API is not yet wired to annual. Clicking the CTA right now creates a monthly subscription regardless of what the toggle says. This is intentional — ship the UI first, wire the backend next session.

That sequencing is fine pre-launch because nobody is actually checking out. The risk of showing $24/mo and charging $30/mo is zero when there are no real users. Post-launch this order would be backwards — you'd ship the backend change first and toggle the UI only after you've verified the Stripe integration end-to-end. That's Day 38's job.

Instant Alerts feature page

The Product dropdown has had six items in it since day one. Five of them have been linking to #. That stops being okay once people start finding the site through search or word of mouth — clicking a nav item and going nowhere is a trust hit.

/features/instant-alerts is now live. It's the first of the six feature pages to have a real destination.

The page does one job: explain the feature clearly enough that a skeptical salon owner understands why they want it before they've signed up. The copy angle matters here. "Get notified instantly when a review comes in" is the feature. It's not the reason someone would pay for it. The reason is: most business owners find out about a bad review from a friend, a walk-in customer, or a lost sale — days or weeks after it happened. By then the damage is done and the window to respond helpfully is closed.

The page leads with that pain, not the solution. The solution (instant alerts) is what you offer after you've established that the current situation is bad. Standard problem-solution structure, but it's easy to skip when you're building fast and just want to ship a page.

The SiteHeader nav was updated desktop and mobile — Instant Alerts now resolves to /features/instant-alerts instead of #. Five more to go.

Process note: the tsc garbage file

This one's worth logging because it's the kind of mistake that would have committed junk into the repo if I hadn't caught it.

During the Day 37 session, the type-check step was supposed to run npx tsc --noEmit as a shell command. Instead, a file was created with that string as its filename — the command was treated as a file path rather than executed. The file had to be unstaged and deleted before committing.

The fix going forward: every prompt that includes a type-check step needs to explicitly say "run tsc as a shell command, do not create a file named after the command." It sounds obvious. It wasn't. Worth writing down once so it doesn't happen again.


Day 38 is checkout wiring — the toggle needs to actually do something when someone clicks it. The Stripe integration is half-done; the annual price IDs exist, the UI exists, the backend just needs to read the right one. That's the whole task. Should be a contained session.

If you're following along: /roi-calculator is a good way to pressure-test whether the pricing makes sense for your business before committing to annual.

Written by

The founder of Ominvo

Building review management for single-location small businesses. Join the waitlist →