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.

- 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
| App | Role | Stack |
|---|---|---|
| client | Public storefront and sell funnel | Next.js 16, React 19 |
| admin | Back office, CRM, and analytics | Next.js 16, React 19 |
| agent | Field app for pickups and inspections | Next.js 16, React 19 |
| server | Shared REST API, sessions, realtime | Express, MySQL, Socket.IO |
| sellprime-app | Customer mobile app | Expo 54, React Native |
| redirector | Legacy hostname redirects | Plain 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.
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.
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.
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.
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.
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.
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