A ₹7,000 discount can still be a bad deal

I built HustleRadar because a crossed-out price is not the same thing as a profitable resale opportunity.

₹15,000 against a displayed reference price of ₹22,000 looks like an instant ₹7,000 saving. But a reseller still has to answer harder questions:

  • Is the reference price meaningful, or was it inflated for the sale?
  • What are other stores charging for the exact same variant today?
  • What price could I realistically sell it for?
  • What remains after marketplace fees, delivery, coupons and other costs?
  • Is this a real opportunity, or just a convincing product page?

The problem in one screen

These are the failure modes the product is designed to catch before someone spends money on inventory:

HustleRadar problem-card carousel showing six ways flippers lose money

The landing-page carousel introduces “Six ways flippers lose money.” Its selected card highlights an inflated “Sale” buy price, while the surrounding cards point to missing price insight, fees that erase the margin and the possibility that a cheaper source exists. It frames the product problem before the evidence-backed comparison begins.

HustleRadar turns those questions into one evidence-backed workflow: enter a product and an offer price, compare the market, calculate the exit margin, then ask an open-weight model to explain the result.

HustleRadar report showing a deal verdict, retailer prices, market median and resale margin

The report above is a labelled, reproducible demo scenario. It is included to show the product flow; it is not presented as a claim that those particular prices are live today.

The short version

The application is built for people who buy expensive inventory with their own money: electronics flippers, retail arbitrage beginners, freelance studios, photographers, video editors and small e-commerce operators.

The promise is deliberately narrow:

Do not buy because a product page says “discount”. Buy only after you can see the market, the fee-adjusted exit price and the evidence behind the recommendation.

What I Built

HustleRadar is an open-source market evidence and arbitrage decision tool. It is not a general-purpose shopping chatbot and it does not pretend that a language model can see the web by itself.

The user enters:

  1. A product name or product URL
  2. The price they would actually pay
  3. An optional original/reference price
  4. A server-side evidence mode: live for the investigator flow, or explicit demo fixtures for reproducible testing and previews

The product then runs a complete decision loop:

  1. Collect evidence from shopping results, selected retailers and dated context.
  2. Normalize the product identity so accessories, refurbished listings and wrong variants do not pollute the comparison.
  3. Calculate the market picture: lowest observed price, median price, source coverage and fee-adjusted resale margin.
  4. Show the evidence with retailer names, links, prices, timestamps and coverage warnings.
  5. Use Google Gemma 2 to turn those structured facts into a short, practical explanation for a buyer or side hustler.
  6. Fall back safely to a deterministic evidence-backed rationale if the model is disabled or unavailable.

1. Landing page: start with a product, not a conversation

The landing page is intentionally small and action-oriented. A reseller can paste a product name or product link, enter the real buy price, optionally enter the displayed/reference price and start the check without writing a prompt.

HustleRadar landing page with the Find The Flip hero and Find a Flip action

The landing page presents the Find The Flip Before Anyone Else hero, the primary Find Flip action, and the product promise: live SerpApi retailer scans, Gemma 2 analysis and no browser extension requirement. This is the product entry point, not a deal verdict.

The interface also makes the boundary visible: live data, multiple retailers, Gemma 2 AI and local inference availability are capabilities of the system, not a guarantee that every provider will always respond.

2. Compare the market instead of trusting the banner

HustleRadar compares the product across multiple stores and keeps the source attached to each number. The point is not to produce the biggest discount percentage; it is to find the price floor and the price range a real buyer may encounter.

HustleRadar price comparison view showing prices by store

3. Make the resale calculation explicit

The report separates the numbers that are usually mixed together on a product page:

  • sourcing price — what the user pays
  • market floor — the lowest comparable listing found
  • market median — a less fragile view of the market than one outlier
  • estimated resale fee — currently modelled as an 8% marketplace fee
  • net resale margin — what remains after the fee and sourcing cost

HustleRadar arbitrage report showing resale margin and flip feasibility

The simplified calculation is:

estimated net profit = expected resale price × (1 − marketplace fee) − sourcing price 

Delivery, coupons and bank discounts can be modelled in the scenario calculator rather than hidden inside a single optimistic percentage.

HustleRadar scenario calculator for bank discounts, delivery fees and coupons

4. Show why the recommendation exists

The most important part of an AI recommendation is not the adjective “good” or “bad”. It is whether a person can inspect the path from source evidence to conclusion.

The evidence view exposes the coverage and provenance behind the report:

HustleRadar evidence coverage view showing source status and market evidence

That makes the app useful even when the answer is “insufficient data”. A missing market observation is safer than a confident-looking number invented to complete the card.

5. Protect the comparison from near matches

“MacBook M5” should not be compared with a MacBook M4, a MacBook Pro, the wrong screen size, a different memory/storage configuration or an accessory. HustleRadar applies product, variant and condition checks before prices enter the calculation.

HustleRadar variant selection screen for narrowing the exact product match

This is a small detail with a large effect: a cheaper 13-inch or lower-storage configuration can make an apparently excellent margin if it is allowed into a 15-inch or higher-storage MacBook comparison.

Demo

Live mode, demo mode and honest failure

You can try the application at hustleradar.onrender.com. The walkthrough is on YouTube.

The API has two explicit evidence modes:

  • Live mode calls the configured search providers and reports the provider status it actually receives.
  • Demo mode loads clearly labelled fixtures so the complete UI can be demonstrated without an API key, quota or unstable retailer pages.

The screenshots in this article use the stable demo scenario where indicated. That is intentional: the article should remain reproducible instead of quietly turning a quota failure into a fake live result.

If a live provider fails, the report remains live, has no fabricated demo sellers, and can return insufficient_data. Demo fixtures are only loaded when the caller explicitly requests mode: "demo".

The backend supports both modes, while the current investigator flow submits live scans by default. The landing page's demo visuals and the offline tests use clearly labelled fixtures; the live search screen never silently changes a failed provider response into demo evidence.

Render health endpoint and deployment verification for HustleRadar

Code

Repository

The complete source is available here:

github.com/DeepSaha25/HustleRadar

The most relevant implementation files are:

Evidence collection stays separate from the verdict

The backend collects three evidence streams concurrently. The model does not replace this work:

const [shopping, retailers, context] = await Promise.all([ run("shopping", () => searchShoppingDetailed(query, signal, forceRefresh)), run("retailers", () => searchMajorRetailersDetailed(query, signal, forceRefresh)), run("context", () => searchContextDetailed(query, signal)), ]); return { query, mode: "live", shopping, retailers, context, }; 

Each batch retains its status. A successful empty search is still an observation; a provider error is not converted into a successful result.

Demo fixtures require an explicit mode

The important safety boundary is small but deliberate:

if (mode === "demo") { const evidence = fixture(query); setEvidence(cacheKey, evidence); return { evidence, fromCache: false }; } const evidence = await collectLive(query, signal, progress, forceRefresh); 

This prevents a failed live search from being “helpfully” filled with demo data. It also makes cached evidence mode-specific, so a demo report cannot leak into a live request.

Gemma receives facts, not a blank prompt

The prompt sent to Gemma contains the normalized product, verdict, claimed price, reference price, market median, lowest comparable listing and calculated arbitrage result:

const prompt = `You are HustleRadar's open-source deal auditor and retail arbitrage advisor powered by Google Gemma 2. Evaluate this product deal strictly using the verified market evidence below. Do NOT hallucinate prices or retailers. Product: ${params.productTitle} Verdict: ${params.verdict} (Confidence: ${params.confidence}) Claimed Price: ${formatPrice(params.claimedPrice)} Market Median Price: ${formatPrice(params.medianPrice)} Lowest Listing: ${params.cheapestSeller?.name} Arbitrage Net Profit Potential: ${formatPrice(params.arbitrage?.netProfit)} `; 

The model is asked to explain two decisions:

  1. Is the advertised discount genuine or inflated?
  2. Is there viable resale margin after the estimated marketplace fee?

That is a much narrower and more auditable job than asking a chatbot to browse the internet and guess.

A model failure should not erase the arithmetic

Gemma can be disabled with ENABLE_LLM=false. If Ollama or an optional open-model endpoint is unavailable, HustleRadar returns a deterministic rationale generated from the same structured evidence and calculations.

The model can improve the explanation. It does not own the underlying prices, fee calculation or product-match decision.

Deterministic arbitrage math comes before language generation

The margin is calculated in TypeScript before Gemma is called. The current product models an 8% resale-platform fee and classifies the opportunity using both percentage margin and absolute net profit:

const resaleMedianPrice = medianPrice; const grossMargin = resaleMedianPrice - buyPrice; const marginPercent = Math.round((grossMargin / buyPrice) * 100); const platformFeeEst = Math.round(resaleMedianPrice * 0.08); const netProfit = grossMargin - platformFeeEst; if (grossMargin > 0 && netProfit > 0) { opportunityScore = marginPercent >= 20 && netProfit >= 2000 ? "HIGH" : marginPercent >= 10 || netProfit >= 800 ? "MODERATE" : "LOW"; } else { opportunityScore = "UNPROFITABLE"; } 

This is the boundary that makes the product useful for a money-sensitive decision. Gemma can make the result easier to understand, but it cannot change the arithmetic by writing a more persuasive paragraph.

Gemma routing is optional, replaceable and testable

The local model path is deliberately configuration-driven:

if (/^(?:0|false|off|no)$/i.test(process.env.ENABLE_LLM?.trim() ?? "")) { return null; } const gemmaUrl = process.env.GEMMA_API_URL || (process.env.OLLAMA_BASE_URL ? `${process.env.OLLAMA_BASE_URL}/api/generate` : "http://127.0.0.1:11434/api/generate"); const model = process.env.GEMMA_MODEL_NAME || process.env.OPEN_MODEL_NAME || "gemma2:9b"; 

The request uses stream: false; the product streams evidence progress, not Gemma tokens. If local Ollama is unavailable, the implementation can try configured open-model endpoints and finally returns the deterministic rationale. The offline test suite disables LLM calls and verifies that the result still contains a useful rationale.

Three SerpApi paths, one evidence contract

HustleRadar does not treat a single search response as the market. It runs three evidence paths concurrently:

const [shopping, retailers, context] = await Promise.all([ run("shopping", () => searchShoppingDetailed(query, signal, forceRefresh)), run("retailers", () => searchMajorRetailersDetailed(query, signal, forceRefresh)), run("context", () => searchContextDetailed(query, signal)), ]); 

Those paths mean:

  • google_shopping for structured offers;
  • retailer-restricted Google searches for direct store coverage;
  • dated Google context for price mentions and historical signals.

Every request is server-side, uses gl=in and hl=en, applies a bounded timeout and preserves a provider status. A SerpApi HTTP 200 containing an error, quota response or invalid payload is not classified as a successful empty scan.

A listing must earn its way into the median

Before a seller can influence the report, it passes product identity, condition, variant, currency, URL and availability checks:

const assessment = assessListing(query, item.title); if (!assessment.accepted) { excludedCount++; continue; } const variant = listingVariant(query, item.title); if (!variant.complete || !plausibleProductPrice(query, item.price)) { excludedCount++; continue; } 

The same layer rejects accessories, books, replacement parts, refurbished mismatches, foreign currencies and wrong RAM/storage/screen configurations. If multiple configurations remain, HustleRadar stops and asks the user to select one instead of mixing them into a false median.

The browser receives real progress events

The stream endpoint writes one NDJSON object per line and sends a final result only after deterministic analysis is complete:

res.setHeader("Content-Type", "application/x-ndjson; charset=utf-8"); const write = (payload: unknown): boolean => { if (!connected || res.writableEnded || res.destroyed) return false; res.write(`${JSON.stringify(payload)}\n`); return true; }; const result = await execute(req.body, controller.signal, (event) => { write(event); }); write({ type: "result", data: result }); res.end(); 

The React client reads the response body incrementally. That is why the loading screen can show shopping, retailer, context and analysis stages without using a fake timer. Disconnecting the browser aborts the provider work through an AbortController.

How I Built It

HustleRadar is built around a simple architectural rule:

Use open-weight AI for synthesis, but make the source evidence and the important arithmetic inspectable without trusting the model.

The stack

Layer Technology Responsibility
Interface React 18, TypeScript, Vite, Tailwind CSS Product input, streamed progress and decision report
API Node.js, Express and TypeScript Request validation, orchestration and report assembly
Search grounding SerpApi shopping and retailer search Current offers, retailer evidence and dated context
Product guardrails Deterministic matching and INR parsing Reject wrong models, accessories, refurbished items and non-INR prices
Decision engine Deterministic median, fee and scenario calculations Compute the numbers before language generation
Open model Google Gemma 2 through Ollama or supported endpoints Explain the evidence for a buyer or reseller
Transport NDJSON streaming Show shopping, retailer and context progress as it happens
Deployment Render Blueprint Serve the frontend and Node API from reproducible configuration
Verification Node test runner and production builds Protect the important boundaries offline

The architecture

flowchart TD A[Browser<br/>React + TypeScript + Vite] -->|POST /api/check-deal/stream| B[Node + Express API] B --> C[Query and URL validation] C --> D{Evidence mode} D -->|Live| E1[SerpApi shopping search] D -->|Live| E2[Retailer-specific searches] D -->|Live| E3[Dated context search] D -->|Demo| E4[Explicit demo fixtures] E1 --> F[Evidence batches<br/>status, prices, links, timestamps] E2 --> F E3 --> F E4 --> F F --> G[Product, variant,<br/>condition and INR filters] G --> H[Median, fee and<br/>scenario calculations] H --> I{Gemma enabled<br/>and available?} I -->|Yes| J[Gemma 2 rationale<br/>constrained by evidence] I -->|No or unavailable| K[Deterministic rationale<br/>fallback] J --> L[Decision report] K --> L L --> M[Verdict and confidence] L --> N[Price comparison matrix] L --> O[Arbitrage margin] L --> P[Evidence receipts] L --> Q[Warnings and provider coverage]

1. Ground the model with multiple search paths

One search query is not enough for a resale decision. HustleRadar uses separate paths for:

  • Shopping results — structured offers and merchant prices.
  • Major retailer searches — direct retailer pages that an aggregator may miss.
  • Context search — dated mentions that help distinguish a current market price from a temporary marketing reference.

The result is stored as evidence first. The application can then show the source, status and timestamp instead of reducing everything to an unsupported answer such as “great deal”.

2. Normalize the product before calculating a margin

The hardest part of price comparison is often not arithmetic. It is deciding whether two listings describe the same thing.

The validation layer rejects or flags:

  • the wrong model or generation, such as a MacBook M4 or MacBook Pro result for a MacBook Air M5 query;
  • accessories such as cases, cables and replacement ear pads;
  • refurbished or used listings when the requested product is new;
  • mismatched storage, RAM, size or colour variants when those attributes matter;
  • prices that are not valid Indian rupee values.

This is why a cheap accessory cannot accidentally become the “lowest seller” for an expensive product.

3. Calculate the decision before asking Gemma to write it

The recommendation engine calculates the market median, the lowest matching seller and the fee-adjusted resale outlook first. This creates a stable baseline that can be checked in tests and displayed in the UI.

The language model then answers a smaller question: how should these verified facts be explained to a person deciding whether to buy?

That separation is the product's main design choice. It gives a small open model a useful job without making it responsible for discovering or inventing the market.

4. Keep the model replaceable

The verified local model run used gemma2:9b through Ollama at http://127.0.0.1:11434. The code also supports configured open-model endpoints, while the deterministic path remains available when the model is disabled.

The model name, endpoint and enable/disable behavior come from environment variables rather than hard-coded credentials. Secrets stay in .env or deployment configuration and are not committed to the repository.

5. Stream progress instead of showing a frozen spinner

Search takes longer than a local calculation. The API emits NDJSON progress events while it checks shopping results, retailers, context and analysis. The frontend can show which evidence path is running, which one finished and where a provider returned an error.

That small interaction detail matters for trust: the user can see that the report is being assembled rather than waiting for an opaque “AI answer”.

6. SerpApi is treated as a provider, not as truth

The request adapter adds the Indian market and English-language parameters on the server, bounds the request time and rejects provider payloads that contain an error even when the HTTP status is 200:

const response = await axios.get("https://serpapi.com/search.json", { params: { ...params, gl: "in", hl: "en", api_key: apiKey }, timeout: providerTimeout(), signal, maxContentLength: 2_000_000, maxRedirects: 0, }); const data = record(response.data); if (!Object.keys(data).length || data.error || record(data.search_metadata).status === "Error") { throw new Error("provider-response-error"); } 

Shopping rows must contain a real allowlisted retailer product URL and a usable INR price. Foreign currency, EMI/monthly prices, “from” ranges and ambiguous promotional text are excluded instead of being treated as the checkout price.

7. Direct retailer verification closes the search loop

For retailer-restricted results, the backend does not blindly trust the search snippet. It discovers candidate pages, verifies them on the retailer domain and keeps the source class visible:

const relevant = parsed.items.filter((item) => retailerForUrl(item.link)?.domain === store.domain && discoveryListingMatches(query, item.title), ); const candidates = [...new Map(relevant.map((item) => [item.link, item])).values()].slice(0, 3); const verified = await mapConcurrent( candidates, 2, (item) => verifyRetailerListing(item, query, signal, forceRefresh), ); 

The UI can distinguish “Read from the store page”, “Found on Google Shopping”, “Found in Google search” and “Demo price”. That makes SerpApi a grounding layer with receipts rather than an opaque source of numbers.

8. Render runs a two-service deployment

The selected Render category is supported by a checked-in Blueprint, not just a badge:

services: - type: web name: hustleradar-web env: static buildCommand: npm install --include=dev && npm run build --workspace=apps/web staticPublishPath: ./apps/web/dist - type: web name: hustleradar-api env: node plan: starter buildCommand: npm ins...
Source:https://dev.to/deep_saha/hustleradar-live-resale-evidence-with-serpapi-and-gemma-2-1dg6