A deal-analysis tool that reads a listing the way a PE analyst would.

Most first-time business buyers hand a listing to their accountant, get back a stack of ratios they don’t understand, and make the biggest financial decision of their life on a gut feeling. DealLens does both halves of that job at once — the numbers and the translation.

Role
Sole operatordesign · build · ship
Stack
Next.js 15 · SupabaseOpenRouter · 3 models
Scope
Full SaaSauth · billing · AI pipeline
Org
DealLenssolo build
Shipped
Q2 2026launch-ready
deallens.app/analyze/a8f2AP · Pro
Analysis
Metrics
Red flags
Memo
Saved
BIZBUYSELL / LISTING #184220 / Q2 2026

HVAC contractor · Tulsa — $1.4M asking

Scraped, extracted, calculated, flagged, and memoed in 52 seconds.
SDE
$412k
DSCR
1.32
Cash-on-cash
28%
FLAGCustomer concentration — top-3 clients = 61% of revenueReview
FLAGOwner runs sales personally — transition risk on handoffReview
MEMOExpert · 150 words · ends with PROCEED WITH CAUTIONReady
MEMOLearner · 500 words · 2nd person · benchmarks + analogiesReady
Live atdeallens.vercel.app

01 The thesis

What a PE firm does, for a normal person. That’s the whole product, in one sentence.

Institutional buyers have analysts, diligence checklists, and decades of pattern recognition. First-time buyers have a spreadsheet, a broker, and a nagging feeling that they’re missing something. The existing tools on the market either assume you already know what SDE is, or they hide the math entirely and hand you a grade. Neither helps you learn.

I wanted a tool that shows the number and teaches you how to read it — and lets you turn off the teaching once you don’t need it anymore.

Every metric on the screen needs two versions. One for the buyer who knows what DSCR is, one for the buyer who’s about to learn. Every screen, every AI output, every memo. That’s the moat.
— Thesis, in one sentence

The coinage: dual-mode

The term I want to own for this product is dual-mode. Dual-mode is the property that a single source of truth — one number, one calculation, one piece of data — drives two distinct renderings: a compact expert view and an expanded learner view with definition, benchmark range, analogy, and a sentence interpreting what that number means on this specific deal.

Dual-mode has to be a schema-levelconstraint, not a UI polish pass. If it’s a late-stage design decision, half the metrics end up without benchmark data, the AI memo has only one voice, and the buyer profile has nothing to personalize against. Built from the schema up, every metric has a definition and a benchmark range by the time the first row is written. Built from the UI down, you spend three months backfilling.

The gate is also dual-mode-shaped: Learning Mode is Pro-only. Free tier is three analyses a month, numbers-only, expert memo. Pro is $29/month for unlimited plus the whole teaching surface. The teaching surface is what someone actually pays to get.

Expert mode
Learning off
DSCR · debt coverage
1.32
vs 1.25 sba min
Acceptable
Hvac · avg 1.4p25 — 1.15p75 — 1.65
Learner mode
Learning on · pro
DSCR · debt coverage
1.32
What it means
For every $1 of loan payment this business makes in a year, it earns $1.32 in cash. The buffer is what keeps you from missing payments in a bad quarter.
Benchmark · HVAC contractors
SBA lenders want ≥ 1.25 to approve. Most PE buyers look for ≥ 1.50.
<1.00 · distress1.25 · sba min1.32 · here1.50 · pe target
Analogy
Like a salary that covers your mortgage with 32% left over— enough that one bad month doesn’t miss a payment, not enough that two bad quarters wouldn’t.
On this deal
Tight but workable. The customer concentration flag matters more than the headline number — a single client departure could cut DSCR under 1.0 in a quarter.
Fig. 01One number, two renderings. Toggle lives at the top of every analysis. Learner side is Pro-gated; free tier sees expert only.

02 The pipeline

One listing URL in. A full analysis out. Under a minute, about seven cents, five distinct responsibilities handed to the model best suited to each one.

The naive version is one big prompt to one big model. That’s what I’d have built two years ago. Today the frontier is cheap enough and narrow enough that the right shape is one small prompt per job, routed to the model that does that job best. OpenRouter makes that routing a one-line config.

Firecrawl wraps the scraping layer (BizBuySell, Acquire.com, Flippa, MicroAcquire). Gemini 2.5 Flash reads the scraped HTML and returns a structured listing object — mechanical work, cheap and fast. Fifteen-ish financial calculations run in pure TypeScript, no AI involved; SDE, DSCR, the SBA loan structure, cash-on-cash, IRR, payback, breakeven, working capital, concentration risk. Gemini 2.5 Pro reads the listing plus calculations and scans for red flags across five categories. Claude Sonnet writes the memo — because writing voice matters, and Claude writes.

ONE URL · ~52 SECONDS · ~$0.07STAGE 01scrapeFirecrawl~8s · $0.01STAGE 02extractGemini 2.5 Flash~6s · $0.005STAGE 03calculatePure TypeScript<1s · $0STAGE 04red-flagGemini 2.5 Pro~18s · $0.02STAGE 05memoClaude Sonnet 4.6~20s · $0.04NO AI IN THE MATH15 pure functions · every one unit-testedGEMINI CHEAP WHERE MECHANICAL · CLAUDE EXPENSIVE WHERE THE VOICE MATTERS · ~22% CHEAPER THAN ALL-CLAUDE
Fig. 02Five stages, four providers, one orchestrator. Each prompt written against a specific model’s strengths — not a generic “LLM.”

Why no AI in the math

One anti-pattern is underlined in the CLAUDE.md file at the repo root, where every session starts: never use AI for financial calculations — all math is pure TS functions, fully unit testable.

Anti-pattern
A model that occasionally miscomputes DSCR is a model that occasionally hands a buyer a wrong go/no-go. The math has to be deterministic, diffable, and testable the way a spreadsheet is testable. AI writes the memo about the number. The number itself is code.

Every calculation function in lib/calculations/has a co-located test file. Adding a new metric is a two-file PR: the function, the test. Claude can write both — it’s just barred from running the math at inference time.

The cost arithmetic on the routing split is the non-obvious payoff. All-Claude would’ve been simpler to reason about and slightly easier to write. It would also have been about 22% more expensive per analysisand noticeably slower on the extraction step. At seven cents per analysis, the margin on a $29/month Pro subscriber is what underwrites the whole business model. Nine cents wouldn’t have.

03 The rebrand that broke the product

Styling is part of the contract. I treated the rebrand as a visual pass. It wasn’t.

Mid-build I redesigned the whole UI to a warmer palette — amber on brown, a “Warm Terminal” look that matched the product’s craftsman voice better than the original indigo. The design work was satisfying. The commit a week later was not: “fix: 7 post-rebrand fixes — auth, analysis flow, metrics, dashboard cleanup.” Which is a polite way of saying the rebrand shipped broken.

Auth redirects referenced old class names. The analysis processing page lost its loading state because the spinner was styled on a DOM ID that had been renamed. Dashboard filtering rules were tied to old data-attributes. The app looked right on the landing page and was subtly broken from the second screen onwards.

Before · indigo
deallens.app
DSCR
1.32
Analyze
SaaS template voice. Correct. Forgettable.
After · warm terminal
deallens.app
DSCR
1.32
Analyze
Craftsman voice. Matches “PE firm, for a normal person.”
Fig. 03The swap took a weekend. The clean-up after took a week. The visual was the easy part.
Scarsfour lessons paid for in commits
#1
The rebrand shipped broken

Seven routes referenced old class names, old DOM IDs, old data-attributes. The landing page and the dashboard looked right. Auth, analysis flow, and the metric grid did not. Shipping a visual pass without running the full E2E suite meant the first people to test after the commit were users.

A visual redesign touches the functional layer whether you plan for it or not. The right response is to run the full Playwright suite after any design pass, not to eyeball the landing page and call it done.

#2
Four bugs in the AI pipeline, shipped at once

fix: resolve 4 bugs breaking the AI analysis pipeline — a commit message that should embarrass me, and does. Three of the four LLM calls had to be debugged after the Phase 5 merge. The root cause was that I’d written the prompt functions in isolation with canned inputs, and never run the full orchestrator end-to-end before merging.

“Calls three LLMs in sequence” is a distinct class of bug risk. Unit tests on each prompt pass and the integration still fails — the bugs live in the hand-offs between steps, not in the steps themselves. Treat the orchestrator as the thing that needs its own integration test, not a glue layer you trust.

#3
3,692 lines of dead code

The pre-launch audit removed almost 4,000 lines of dead code from a codebase of ~25k total. Most were stubs from design experiments that stayed on disk after I pivoted away. I’d been telling myself I’d clean it up later for three phases.

Delete in the same commit that abandons the file. Dead code accumulated across phases is expensive to audit — both for humans and for the AI collaborator reading the repo at session start. The rule I try to hold now: if I haven’t touched a file in two weeks and it isn’t load-bearing, the next commit deletes it.

#4
learning_mode defaulted to true

Learning Mode is a Pro-only feature — the whole teaching surface. I defaulted it to truefor free users early on, which meant the entire moat was showing to people who hadn’t paid for it. Trained the wrong expectation. Caught in the gate matrix rewrite and now called out by name in the anti-patterns list.

A gate is a default. Whichever value the code ships with is the one that defines what free means. The fix wasn’t a conditional — it was moving every gate check into lib/gates.ts as a single source of truth, with the default explicit in one place instead of implicit across twelve components.

04 The threat model

If your threat model is “me on localhost,” your threat model is wrong.

The first security hardening pass caught eleven vulnerabilities. A second pass caught six more. None would have mattered on day one with ten users. All would have mattered on day one with ten thousand.

The fix wasn’t a library or a scanner. It was a question I started asking of every API route: what happens if a hostile user calls this?Asked once, at the route level, for every route. Most routes passed. The ones that didn’t broke in surprisingly similar shapes.

EndpointAssumed callerActual callerFix
/api/cron/*Vercel cron, trustedAnyone with the URL, unauthenticatedAuthorization: Bearer CRON_SECRET on every route; reject on mismatch.
pg function · run_usagePostgres default search_pathCaller-controlled search_path → schema substitutionSET search_path = public, pg_temp pinned on every SECURITY DEFINER function.
/api/events · deal_revisitedFires once per visitClient loop → metric inflated arbitrarilyServer-side dedupe by (user_id, deal_id, hour); throw away client counts.
/api/checkout · return_urlOur own domainUser-supplied → open redirect to phish via StripeAllowlist regex on return_url; strip any cross-origin prefix.
/api/webhook/stripeStripe signed itAnyone can POST JSON that looks like an eventconstructEvent() with the webhook secret; reject before touching the DB.
env loaderAll vars present in prodMissing var → undefined silently shippedZod schema on lib/env.ts; crash on boot if a required var is missing.
Fig. 04Every row is the same shape: an implicit assumption about who the caller is, and an explicit check that enforces it.
Scarthe one that was embarrassing to find
#5
The deal_revisited inflation bug

The deal_revisitedevent fed the buyer-profile personalization loop — it’s how the system learns which deals a user actually comes back to. Fired client-side from a useEffect. Any user viewing their own analysis in a loop could inflate their own signal arbitrarily, and since the learning loop was downstream of that metric, a motivated user could steer their own personalization — or worse, aggregate metrics for cohorts they happened to be in.

Never trust a client-fired analytics event as a metric. Treat events as hints; aggregate and dedupe server-side before anything downstream reads them. The general rule I took from this: write down what each endpoint assumes about the caller, and check whether the assumption is enforced or just implied.

05 What it adds up to

Shipped. Eight phases in main. The remaining work is configuration, not code.

The intelligence loops are live — a quarterly benchmark scrape so multiples stay current, and a passive buyer profile that personalizes the learner memo from behavioral telemetry. The pre-launch audit is done. What’s left is live Stripe keys, DNS, Supabase production auth URLs. Thirty-eight checklist items verified; thirty configuration items remaining.

Phases shipped
8/8
Full plan in main. Core + intelligence loops + pre-launch hardening.
Cost per analysis
~7¢
22% below all-Claude. Enough margin to underwrite $29/mo Pro.
Pre-launch cleanup
17
Vulnerabilities patched across two security passes. 3,692 lines of dead code removed.
E2E coverage
87
Playwright specs, including the orchestrator integration tests I wrote after the four-bug commit.

The product lesson I keep coming back to: the moat isn’t the AI, it’s the dual-mode schema. Anyone can wire up OpenRouter and ship a memo generator in a weekend. What they’d have to rebuild from the schema up is the teaching surface — every metric with its benchmark range, every memo with two registers, every gate with the correct default. That’s not a weekend. That’s the product.

The meta-lesson: I now know what it feels like to hold the full shape of a SaaS app in my head at once. Auth, billing, scraping, multi-model AI, structured-output validation, RLS, rate limiting, cron, email, design system, E2E tests. None of them are hard individually. Holding all of them at once, and knowing which are load-bearing for launch and which can wait, is the thing that’s hard to teach — and the thing AI collaboration made possible for a team of one.

Shipped.

06 The share

If the moat is the schema, the math is the gift. So I gave it away.

The week after launch I extracted the screening engine — the calculations, the red-flag taxonomy, the memo structures, the language posture — into a standalone Claude Skill and pushed it to GitHub under MIT. Anyone with a Claude account can install it in under thirty seconds and run a PE-style screening on a BizBuySell listing in their own chat. No login, no billing, no paywall.

That sounds like a contradiction until you remember chapter 01. The moat was never the math. The math is fifteen functions any competent engineer could write in a weekend. The moat is the teaching surface— every metric with its definition and benchmark range, every memo with two registers, every gate with the correct default. That’s the schema-up build that takes months. None of that ships with the skill.

Funnel logic
Every memo the skill produces ends with the same footer — “for saved deals, AI Research Assistant chat with web access, PDF exports, and benchmark data, see deallens.fyi.”Whoever installs it meets the brand voice first, and gets routed to the SaaS for the parts the skill can’t do.

What ships, and what doesn’t

The skill carries the postureintact — never advisor, always screening, always route to a CPA / attorney / broker before any signature. That posture is the part of the brand I care most about preserving in the wild. The features are the part I’m happy to gate.

LayerIn the free skillIn DealLens Pro
calculationsSDE multiple, DSCR, cash-on-cash, payback, SBA loan amortization — deterministic, unit-testedSame engine, plus an interactive scenario modeler with debt and down-payment sliders
red-flag screen~20-flag checklist across financials, ops, legal, market, seller categoriesSame checklist, plus AI Research Assistant chat withweb access for industry comps
memoExpert and Learner memo templates — one at a timeDual-mode rendering side-by-side, plus PDF export with professional formatting
benchmarksNot included — bring your own rangesIndustry SDE multiples, gross margins, growth rates, kept current via a quarterly scrape
memoryStateless — one chat, one analysisSaved-deal dashboard with revisit history; buyer-profile learning that personalizes future analyses
postureIdentical — never advisor, always screening, always route to a CPAIdentical. The language rules are the brand.
Fig. 05Six layers, one division: features below the line are gated; the posture above the line is not.
Try it~30 seconds, no account beyond Claude
  1. 01
    Claude Code
    Drop the deallens-lite/ directory in ~/.claude/skills/. Auto-loads on next session.
  2. 02
    Claude.ai · desktop
    Settings → Skills → Add skill → point to the directory.
  3. 03
    Project-scoped
    Place under <project>/.claude/skills/deallens-lite/ — loads only for that project.
Then ask Claude“Screen this BizBuySell listing for me — [paste]”
Next case