One number is your minimum job price. GroundCut enforces it everywhere.

Every other lawn-care tool makes you tune a minimum job price AND a separate per-visit base for subscriptions. Two fields, same idea, both go out of sync. GroundCut collapses them into one: the Starting price floors small polygon quotes and anchors your recurring monthly bill.

A tenant-wide target hourly rate is the second safety net — it flags any quote that clears the floor but still wouldn’t hit your hour.

How the floor works in the pricing engine

Every step here is shipped code in packages/shared/src/pricing.ts and rides on the /api/quote response.

  1. 01

    One number is your minimum AND your recurring anchor

    Every other lawn-care tool makes you tune a per-service "minimum price" AND a separate "base price" for subscriptions. Two fields, same idea, both have to stay in sync or your math drifts. GroundCut collapses them. Each service has one number — Starting price. It floors the polygon quote when the area math comes in low, and it anchors the recurring monthly bill (× 4 weekly, × 2 biweekly, × 1 monthly). No twin fields, no drift.

    services.base_price (NOT NULL column on services)

  2. 02

    The floor wins over the area math, every time

    The quote engine computes a price from `price_per_quarter_acre × (mowable_area_sqft / 10,890)`. Then it applies the overgrown surcharge if flagged, the distance fee, and any seasonal adjustment. Then `Math.max(price, base_price)` runs — the Starting price cannot be undercut. A customer who drew a 200-sqft lawn doesn’t pay $0.83; they pay your floor. The bump is absorbed into the area line item so the breakdown still sums cleanly on the customer-facing quote.

    packages/shared/src/pricing.ts — Math.max(price, input.base_price)

  3. 03

    Max price routes oversize lots away from auto-booking

    Pricing rules also have a `max_price` ceiling. A customer outlining an absurd area gets clamped to the cap — the booking flow doesn’t produce a $4,000 instant quote that locks you into a job you can’t deliver. Big lots route to the in-person quote flow instead, where you visit the property, fill in line items, and send a real quote. Floor and ceiling are the only two guardrails you set per service.

    services.pricing_rules.max_price — Math.min(price, rules.max_price)

  4. 04

    Target hourly rate catches profitable-looking jobs that aren’t

    A quote can clear your $50 Starting price and still be unprofitable — a 90-minute job that nets $48.55 after Stripe is $32/hr, not your $50 target. The engine divides `net_after_fees` by `estimated_time_mins / 60` and returns `below_target_rate: true` on the quote response. The target is one tenant-wide number set in `/admin/settings` — most operators have one $/hr they need to clear across the whole business, not different ones per service. The floor protects against $1 quotes; the target rate protects against floor-priced jobs that still won’t clear your hour.

    tenants.settings.target_hourly_rate → below_target_rate on /api/quote response

  5. 05

    Card-fee passthrough is opt-in and respects the floor

    If you turn on `pass_card_fee_to_customer` in tenant settings, the engine adds Stripe’s 2.9% + 30¢ on top of the quoted price so you net the full quoted amount. The floor still applies to the pre-passthrough price — the passthrough is layered on after, never used to dodge the floor. `card_fee_included: true` rides on the quote response so the booking page can display "$X (card fee included)" without confusion.

    pass_card_fee_to_customer flag; fee added after the base_price floor in pricing.ts

“What about customers who want a discount?”

The discount conversation comes up in every lawn business. A commercial account wants a per-job rate below your residential floor. A long-time customer wants a “loyalty rate.” Your neighbor wants the buddy price. The Starting price floor sits in the booking-page flow — it doesn’t have to apply to every customer.

Recurring price overrides take precedence. Set a recurring schedule with a lower price for that customer and the booking-page floor never enters the picture — the recurring schedule carries its own price snapshot through every generated job. The floor is for new-customer instant quotes, not for established relationships.

customer_price_override on a one-off job. From `/admin/jobs` you can create a job with a custom price; the engine clamps it at your Starting price so a typo can’t zero the job, but otherwise the override wins. The line breakdown shows “Custom rate” and the booking-page math is bypassed.

On-site adjustments are signed and auditable. The crew can adjust the price on arrival if the customer pushes back; increases require a signature, decreases don’t. Every adjustment writes to the audit log with the actor, the before/after, and the reason.

Where each field lives

  • /admin/services — each service has a Starting price, a per-¼-acre rate, a Max price, and an optional overgrown surcharge. That’s it for instant-quote pricing. Edit one service to test, save, and refresh the booking page to see the new math.
  • /admin/settings — the target hourly rate lives here (tenant-wide). When set, `below_target_rate` rides on every quote response; when blank, the flag is silenced.
  • services.base_price + services.pricing_rules.max_price — the underlying columns. Both required. No separate `min_price` field.
  • POST /api/quote — the runtime endpoint. Returns the full quote shape including the floor-applied price, the line-item breakdown, and the target-rate flag.

Minimum-job-price FAQs

Why is your "minimum price" the same field as your subscription price?+
Because an operator’s mental model is "I won’t show up for less than $X" — and that number is the same whether the customer wants a one-off mow or a weekly subscription. Splitting it into two fields was a leaky abstraction every competitor has: you’d set a $50 minimum and a $45 per-visit base, the two would drift apart over time, and then your weekly subscription would price at 4 × $45 = $180 while your one-time floor said $50. One number, one knob — the polygon floor and the recurring anchor are the same thing.
What about a long-time customer who wants $10 off the minimum?+
Two outs. First, the crew can override the price on-site with the Adjust Price flow in the mobile app — every adjustment is logged with a reason and a signature for increases. Second, you can book that customer manually from `/admin/jobs` and set `customer_price_override` on the line, which uses the override as the quoted price (clamped at your Starting price floor so a typo doesn’t zero-out the job). Recurring schedules carry their own price snapshot from when they were set up, so an existing recurring customer at $40 stays at $40 even if you raise the booking-page Starting price to $50 mid-season.
How does the floor interact with a customer who draws a tiny lawn on purpose?+
They pay your floor — that’s the whole point. The area math returns a sub-floor number, the engine clamps it to your Starting price, and the line items show the bump rolled into the area line so the customer sees a coherent quote. The crew arrives, sees that the real mowable area is bigger than what was drawn, and adjusts the price on-site with a signature for the delta. The floor is a safety net against $1 quotes; the on-site adjust handles the gap between drawn area and real area.
Can I have a different floor for weekly vs one-time customers?+
Sort of. The Starting price is one number that floors one-time polygon quotes AND drives the default recurring monthly bill (× 4 / × 2 / × 1). If you want recurring to be cheaper than one-time, set per-frequency overrides on the service: `monthly_price_weekly_override`, `monthly_price_biweekly_override`, `monthly_price_monthly_override`. Those replace the area math for recurring bookings and act like a frequency-specific floor. So a $55 Starting price + $180/mo weekly override means one-time quotes floor at $55 and weekly subs bill $180/mo (≈ $45/visit) regardless of lot size.
Where does the floor live in the database vs the UI?+
It lives on the `services.base_price` column — required, NOT NULL. The admin UI is at `/admin/services`; each service has a Starting price field. The booking page reads it through `/api/quote` and never directly. The same number drives recurring monthly billing math, so you set one price and both flows use it. No separate `min_price` field exists anymore — that was the simplification.
Does the minimum apply to in-person quotes too?+
No. In-person quote services (`quote_type=in_person`) skip the instant-quote engine entirely. You visit the property, fill in line items in `/admin/jobs/[id]`, set an optional deposit, and send the quote. The floor is an instant-quote concept — it doesn’t apply to a quote you wrote by hand. If you want a floor on in-person work, the workflow is to decline low-dollar in-person requests at intake rather than producing a low-dollar quote.
What if my market is so price-sensitive that the floor loses me bookings?+
Lower it and re-run the math. There’s no hard-coded GroundCut minimum — just whatever you set per service. If you’re seeing high abandon rates at $50 and lower at $40, the floor is doing what it should: telling you where price elasticity lives. Tune the Starting price, then tune your tenant-wide target hourly rate in `/admin/settings` to match — and let the below-target flag tell you which floor-priced bookings still aren’t clearing your hour.
How is this different from just setting a high price-per-quarter-acre?+
Price-per-quarter-acre scales the quote with lawn size — a 0.4-acre lot pays more than a 0.2-acre lot. The Starting price protects the small end: when the area math is below your floor, the floor kicks in. Without a floor, your $20 quarter-acre rate produces a $5 quote for a 250-sqft front strip, and a tap-tap-tap drawing produces $0.50. With the floor, both quote at your Starting price. They’re different jobs (scaling vs flooring) and both belong on the service.

Stop quoting below your floor

14-day free trial. One Starting price per service plus a tenant target hourly rate is the whole setup. Run a few real bookings; see the engine clamp small lawns to your floor and flag the rest.

14-day free trial · No card required · Cancel any time