All work

Device Buyback & E-Commerce Platform

SellPrime

A phone and tablet buyback platform for Bangalore, built as a six-app monorepo: a public sell funnel, a back office that doubles as the company CRM, a field-agent app, a mobile app, and one shared Express API. At the centre of it is a pricing engine that turns a device's age, condition, and accessories into a firm cash quote in a single request.

Visit live siteSource is private
SellPrime: Device Buyback & E-Commerce Platform
Role
Sole developer: architecture, backend, three web apps, mobile app, and infrastructure
Timeline
2024 to present
Status
Live in production

Selling a used phone online usually means filling in a form and waiting for somebody to call back with a number. SellPrime does the opposite: you pick your device, answer a short series of questions about its condition, and the price on screen is the price the agent turns up with.

That promise is the whole product, and it puts all the weight on two things: a valuation accurate enough to honour, and a back office strong enough to run the business behind it. Everything in the system exists to serve one of those two.

Apps in one monorepo
6Apps in one monorepo
Database tables
23Database tables
API route groups
18API route groups
Permission combinations
48Permission combinations

The system

What it is made of

AppRoleStack
clientPublic storefront and sell funnelNext.js 16, React 19
adminBack office, CRM, and analyticsNext.js 16, React 19
agentField app for pickups and inspectionsNext.js 16, React 19
serverShared REST API, sessions, realtimeExpress, MySQL, Socket.IO
sellprime-appCustomer mobile appExpo 54, React Native
redirectorLegacy hostname redirectsPlain Node

The core

A valuation engine where pricing is data, not code

Every purchasable variant of a device, a model at a given storage and RAM, is one row in the catalogue, and that row carries both its base price and its own complete set of depreciation values. Nothing about how a particular model loses value is written in a source file. It lives in the database, which means the team can retune any single model from the admin panel without a deploy, and two models can behave completely differently from one another.

The order the deductions apply in is the part that actually matters. Age resolves first and produces an age-adjusted base; every condition deduction after that is a percentage of the adjusted figure rather than of the original price. Depreciation compounds the way it does in reality instead of stacking additively onto a number the device stopped being worth months ago.

  1. Resolve the variant

    One query joins the variant to its model, brand, and device type, pulling the base price alongside every depreciation value configured for that exact combination of storage and RAM.

  2. Apply age first

    The device's age band (under three months, three to six, six to eleven, or over eleven) selects a percentage that comes off the base price. The result becomes the base that every later deduction is measured against.

  3. Deduct condition as percentages

    Cosmetic grade, a non-original screen, a failed touchscreen, dual eSIM, and a missing box each subtract a share of the age-adjusted base rather than of the original price.

  4. Deduct accessories as flat amounts

    A missing charger, bill, or stylus comes off as a fixed rupee figure instead of a percentage, because their replacement cost does not scale with the phone's value. Each falls back to a default when a variant has not been configured.

  5. Apply device-specific rules

    Apple devices below 80% battery health take a battery-service deduction that no other brand does. Tablets skip the condition tree entirely when they will not power on or have no network, taking a flat configured price instead.

  6. Clamp to a floor

    A phone that cannot make calls takes its own percentage and can never be quoted below a fixed floor. Everything else floors at zero, so no combination of answers can produce a negative offer.

Age bands
Under 3 months · 3-6 · 6-11 · over 11
Cosmetic grades
Flawless · good · average · below average
Percentage deductions
Screen not original · touchscreen failed · dual eSIM · no box · battery service
Flat deductions
No charger · no bill · no stylus
Flat-price overrides
Tablet will not power on · tablet network dead
Per-variant toggles
Whether the network and stylus questions are asked at all

Because the questions themselves are per-variant flags, a cellular tablet is asked about its network and a wifi-only one never is, out of the same code path.

Operations

An admin panel that runs the business, not just the database

The back office is the product for everyone who works there. It covers fifteen distinct areas, and most of them replace a tool the business would otherwise be paying for separately, which is why it ended up serving as the company's CRM rather than sitting next to one.

Order management

The full lifecycle from quote to pickup to payout, with agent assignment, status transitions, generated PDF receipts, and realtime updates over Socket.IO so two staff never act on a stale view.

Device catalogue

Brands, models, and variants with per-variant pricing and depreciation, brand logos, model imagery with versioned cache busting, and hardware identifiers used to match a scanned device to its catalogue entry.

Agent management

Field agents are created here and work from an entirely separate application, with their own authentication and a view of only the orders assigned to them.

User management

Customer accounts, order history, profile completeness, suspensions, and a deletion path that preserves records for accounts with trading history.

Support desk

An IMAP watcher pulls the support mailbox into threaded tickets, staff reply from inside the panel, and the reply goes back out over SMTP on the same thread.

Analytics

Google Analytics and Tag Manager on the storefront, plus a first-party pageview and session pipeline writing to the same database, so funnel questions can be answered with a join instead of an export.

Quote analytics

Which devices get quoted and abandoned, plus a pending-quote queue with per-device follow-up tracking so unconverted valuations can be chased.

Serviceable locations

Pincode and multi-city coverage rules that decide, before a customer invests in the funnel, whether an agent can actually reach them.

Access control

Per-admin permissions down to the individual action

Every admin carries a permission map, and every protected route declares the module and the action it requires. There is no single staff role that unlocks the panel: an account can be given read access to orders, write access to devices, and nothing else at all.

Privilege escalation is closed off deliberately. Promoting somebody to developer sits behind its own separate guard, so a full administrator still cannot mint an account more powerful than themselves unless that specific capability was granted to them.

Modules
Orders · devices · users · agents · admins · support · analytics · logs
Actions
View · create · edit · delete · assign · reply
Escalation guard
Managing developer accounts is a permission of its own
Denial detail
Refusals name the exact module and action that was missing

Every mutation writes to an activity log with the acting admin, the affected user, a severity, a detail payload, the IP, and the user agent, filterable by security, orders, users, devices, agents, quotes, reviews, or bans.

Trust

Reviews only real customers can leave

Completing an order generates a single-use token and emails the customer a review link tied to that specific order. There is no open review form anywhere on the site, so a review cannot exist without a transaction behind it.

The link stays valid for seven days after the order finishes, and a cleanup job clears expired tokens on the same schedule the link itself checks against, so the two can never disagree. Submitted reviews land in a moderation queue; only once approved from the panel do they appear on the public site.

Orders completed before the finish timestamp was recorded have nothing to measure a deadline from, so their links deliberately stay open rather than expiring retroactively.

Security

Making the price list expensive to scrape

The valuation model is the business. Anybody able to enumerate quotes across the catalogue would walk away with the pricing strategy for every device the company buys, so quoting is treated as the sensitive operation rather than the harmless one.

Two thresholds run over every quote request. A rapid burst is blocked outright; a slower but still abnormal volume is allowed through and flagged for a human to look at. Anonymous traffic is tracked by session and IP and results in an automatic IP ban, while a signed-in account is suspended instead. Either way a report is filed with the count, the window, and the identifier, so nothing is banned silently.

The economics are what actually stop it. Reaching the useful data requires an account, an account requires a verified mobile number, and the threshold trips long before a single number has paid for itself, so enumerating the catalogue costs roughly one SIM per handful of devices.

Two-tier thresholds

A tight window blocks immediately; a wider one flags without blocking, so a customer comparing four phones is never locked out for browsing. Both are tunable per environment.

Identity-aware response

Anonymous abuse bans the IP. Authenticated abuse suspends the account instead, because banning an IP on a shared mobile network would take out that customer's neighbours too.

Response obfuscation

Payloads to the public storefront are obfuscated, with admin, agent, auth, and upload paths explicitly excluded so internal tooling stays readable.

Reviewable, not silent

Every trip writes an abuse report with the evidence attached, and bans are managed from the panel like any other record rather than living in a config file.

Architecture

Four clients, one API, no exposed backend

The three web apps never reach the Express API from the browser. Each proxies through its own Next.js route handlers, so the backend URL stays server-side and the browser only ever calls its own origin. The mobile app, which has no such intermediary, talks to the API directly and is treated as an untrusted client accordingly.

The API groups its routes by audience rather than by resource: public, staff, and agent surfaces are separated at the router level before any handler runs. That makes the boundary between them something you can see rather than something you have to remember.

Realtime where it matters

Socket.IO pushes order and quote changes to the admin panel, with allowed origins pinned to the deployed frontend URLs.

Resilient database layer

A dedicated resilience wrapper around MySQL handles reconnection, so a transient database blip does not cascade into a failed customer transaction.

Object storage

Device photos and ID proofs upload to MinIO through the agent app rather than being served out of the application container.

Best-effort CRM sync

New customers, quotes, and orders push to an external CRM keyed on an idempotency id, so all three events land on one lead and a CRM outage can never fail a customer-facing action.

Hard parts

What actually took the time

Pricing operations can change at 9pm on a Sunday

Problem
Market rates for used phones move constantly, and a valuation engine with rates compiled into it makes every price correction a code change, a review, and a deploy.
Approach
Every depreciation factor became a column on the variant row instead of a constant in the calculator. The calculator reads the shape of the pricing model out of the database each time it runs, including which questions to ask.
Outcome
Repricing a model is a form submission. The engine has not needed a code change to accommodate a new pricing rule for an individual device since.

Blocking scrapers without punishing customers

Problem
Rate limiting hard enough to stop a determined scraper also stops the customer genuinely comparing four phones before deciding which one to sell.
Approach
Two separate windows with different consequences (a tight one that blocks and a wide one that only flags), combined with tying the valuable surface to phone-verified accounts so abuse carries a real-world cost.
Outcome
Normal browsing never trips the blocking threshold, while enumerating the catalogue costs one verified mobile number per small handful of quotes.

One backend serving four very different clients

Problem
A storefront, a staff panel, a field app, and a mobile app have different auth models, trust levels, and payload needs, but duplicating the API four times means four places for business rules to drift apart.
Approach
A single Express service with audience-scoped route groups and separate authentication middleware per audience, fronted by per-app Next.js proxies so browser clients never learn the backend origin.
Outcome
Pricing, order state, and permissions have exactly one implementation. Adding the mobile app later required no changes to the existing rules.

Stack

Everything it runs on

Frontend

  • Next.js 16
  • React 19
  • TypeScript
  • Tailwind CSS
  • Turbopack

Mobile

  • Expo 54
  • React Native
  • Expo Router
  • Push notifications

Backend

  • Node.js
  • Express
  • MySQL
  • Socket.IO
  • JWT
  • Session store

Auth & messaging

  • Google OAuth
  • OTP verification
  • SMTP
  • IMAP
  • SMS gateway

Infrastructure

  • Turborepo
  • npm workspaces
  • MinIO / S3
  • Reverse proxy + TLS

Generation

  • PDFKit receipts
  • Sharp image pipeline
  • Versioned asset URLs