{"slug": "i-built-a-tiktok-style-shopping-feed-with-a-recommender-system-in-pure-client-js", "title": "I built a TikTok-style shopping feed with a recommender system — in pure client-side JS", "summary": "Developer Bhaumik Tandan built ThodaSa, a TikTok-style shopping feed for the Indian market with a recommender system that runs entirely in client-side JavaScript. The static site on GitHub Pages uses 13-dimensional product feature vectors and a weighted engagement profile stored in localStorage to personalize the feed without any backend or tracking servers. The project is open source on GitHub.", "body_md": "**TL;DR:** I built [ThodaSa](https://thodasa.com) — a reels-style impulse-shopping demo for the Indian market. You scroll products like Instagram reels, and the feed *learns your taste* — with no backend, no login, and no tracking servers. The whole recommender is ~150 lines of client-side JavaScript. [Code is on GitHub](https://github.com/Bhaumik-Tandan/thodasa).\n\nQuick-commerce apps in India (Blinkit, Zepto, Meesho) figured out something interesting: shopping *is* entertainment. I wanted to push that to its logical end — what if the store was literally a reels feed? One product per screen, full-bleed photo, swipe up for the next dopamine hit, everything under ₹499.\n\nAnd because a feed is boring if it's the same for everyone, it needed a recommender system. The catch: this is a static site on GitHub Pages. No servers. So the recommender had to live entirely in the browser.\n\nEvery product gets embedded as a 13-dimensional feature vector — no ML libraries, just an array:\n\n```\n// [0..7] category one-hot (snacks, beauty, gadgets, home, ...)\n// [8..10] price bucket one-hot (low ≤150, mid 151–300, high >300)\n// [11] deal flag\n// [12] highly-rated flag (≥4.5)\nexport const vecOf = (p) => {\n  const v = new Array(13).fill(0)\n  v[CATS.indexOf(p.category)] = 1\n  v[p.price <= 150 ? 8 : p.price <= 300 ? 9 : 10] = 1\n  if (p.deal) v[11] = 1\n  if (p.rating >= 4.5) v[12] = 1\n  return v\n}\n```\n\nThe user's taste is a weighted running sum of the vectors they engage with, persisted in `localStorage`\n\n:\n\n| Signal | Weight |\n|---|---|\n| Purchase | +10 |\n| Add to cart | +8 |\n| Wishlist | +5 |\n| Share | +4 |\n| Dwell > 4s on a card | +2 |\n| Flick past in < 1.2s | −1 |\n| Un-wishlist | −3 |\n\nDwell time comes from an `IntersectionObserver`\n\non the snap-scroll feed — if you pause on a card, that's a signal; if you flick past it instantly, that's a signal too. Every new session decays the profile by 0.85, so recent taste dominates.\n\nOn each visit, every product is scored:\n\n```\nscore = cosine(profile, vecOf(product))\n      + Math.random() * 0.15          // jitter\n      - Math.min(seenCount, 5) * 0.06 // fatigue penalty\n```\n\nThen the feed interleaves: two \"exploit\" cards (best matches, badged ✨ For you) for every one \"explore\" card (random from the long tail, badged 🎲 Fresh find). Pure exploitation makes an echo chamber; the exploration slots keep the feed a discovery machine.\n\nCold start (fewer than 3 signals) falls back to a hand-curated launch order.\n\n`scroll-snap-type: y mandatory`\n\n+ one `100dvh`\n\ncard per product gets you TikTok-feel scrolling with zero JS scroll handlers.`IntersectionObserver`\n\nis a shockingly good implicit-feedback sensor.Scroll a few beauty products and reload — watch the feed rearrange itself. Then check the \"Your vibe\" widget in the wishlist to see what it learned about you.\n\nRoast the code, star the repo, or tell me what you'd impulse-buy under ₹499.", "url": "https://wpnews.pro/news/i-built-a-tiktok-style-shopping-feed-with-a-recommender-system-in-pure-client-js", "canonical_source": "https://dev.to/bhaumik_tandan_8685cfeaf2/i-built-a-tiktok-style-shopping-feed-with-a-recommender-system-in-pure-client-side-js-320a", "published_at": "2026-08-22 08:42:41+00:00", "updated_at": "2026-08-22 09:14:04.937655+00:00", "lang": "en", "topics": ["machine-learning", "developer-tools"], "entities": ["ThodaSa", "Bhaumik Tandan", "GitHub", "Blinkit", "Zepto", "Meesho", "IntersectionObserver", "localStorage"], "alternates": {"html": "https://wpnews.pro/news/i-built-a-tiktok-style-shopping-feed-with-a-recommender-system-in-pure-client-js", "markdown": "https://wpnews.pro/news/i-built-a-tiktok-style-shopping-feed-with-a-recommender-system-in-pure-client-js.md", "text": "https://wpnews.pro/news/i-built-a-tiktok-style-shopping-feed-with-a-recommender-system-in-pure-client-js.txt", "jsonld": "https://wpnews.pro/news/i-built-a-tiktok-style-shopping-feed-with-a-recommender-system-in-pure-client-js.jsonld"}}