Verified catalogue of mobiles and laptops sold in India, collected from real retail listings (FastAPI backend, React frontend, Postgres/pgvector). - REST API under /api/elec (read-only catalogue; admin endpoints need login) - MCP server (FastMCP) at /mcp/ with list_categories, search_products, get_product and price_history tools - Real ratings and reviews read from product pages and search results - Production Dockerfile (requirements-api.txt, no PyTorch) and .env.production.example; remote database only via an explicit ELEC_ALLOW_REMOTE_DB host/name allowlist - docs/API.md: endpoint and MCP reference with live examples Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
9.9 KiB
Electronics Catalog API
Read-only catalogue of mobiles and laptops sold in India (Tamil Nadu focus), available as a REST API and as an MCP server for AI assistants.
Every product is verified by real listings on at least two retail platforms, and every price, image, rating and review comes with the page it was read from. Nothing is generated.
| Base URL | http://31.97.228.132:8000 (HTTPS address coming: https://catalogue-api.workolik.com) |
| Interactive docs | http://31.97.228.132:8000/docs (try every endpoint in the browser) |
| OpenAPI schema | http://31.97.228.132:8000/openapi.json |
| MCP endpoint | http://31.97.228.132:8000/mcp/ (keep the trailing slash) |
| Auth | None for everything in this document |
| Format | JSON, UTF-8 |
Current data: 43 verified products (15 mobiles, 28 laptops) from Amazon, Flipkart, Croma, Reliance Digital, Vijay Sales, Tata CLiQ, Poorvika, Sangeetha, Vasanth & Co and Viveks.
Conventions
- Money is a decimal string in rupees:
"42999.00", never a float. Parse it as a decimal. - Times are ISO 8601 with timezone:
"2026-09-29T12:54:38.447374+05:30". nullmeans "not stated by any source", not zero. For examplerating: nullmeans no platform publishes a rating.- Errors return an HTTP status with
{"detail": "..."}. Unknown product →404. Bad parameter →422. - Data freshness: the catalogue is refreshed by collection runs, not live per request. Each offer has
observed_at, the time it was last read.
Endpoints
| Method | Path | Returns |
|---|---|---|
| GET | /api/health |
Service and database status |
| GET | /api/elec/categories |
Categories with product counts |
| GET | /api/elec/brands |
Brands in a category |
| GET | /api/elec/products |
Product list with filters |
| GET | /api/elec/products/{product_id} |
Full product detail |
| GET | /api/elec/products/{product_id}/price-history |
Price over time per platform |
| GET | /api/elec/sites |
The retail platforms read |
GET /api/health
curl http://31.97.228.132:8000/api/health
{ "status": "ok", "database": true, "database_name": "loyalycatalogue", ... }
status is "degraded" when the database is unreachable.
GET /api/elec/categories
curl http://31.97.228.132:8000/api/elec/categories
[
{ "slug": "laptops", "name": "Laptops", "product_count": 28 },
{ "slug": "mobiles", "name": "Mobiles", "product_count": 15 }
]
GET /api/elec/brands
| Param | Required | Example |
|---|---|---|
category |
yes | laptops |
curl "http://31.97.228.132:8000/api/elec/brands?category=laptops"
Each item: brand, brand_slug (use it to filter products), product_count, min_price,
max_price, sample_image. Brands with product_count: 0 are allow-listed but have no verified products yet.
GET /api/elec/products
| Param | Example | Meaning |
|---|---|---|
category |
mobiles, laptops |
Category slug |
brand |
samsung, hp |
Brand slug from /brands |
q |
galaxy a56 |
Text search on product and brand name (max 100 chars) |
min_price, max_price |
30000 |
Filter on best_price, rupees |
site |
croma.com |
Only products listed on that platform (domain from /sites) |
limit |
48 |
1–200, default 48 |
offset |
0 |
Paging offset |
Sorted by number of platforms (most first), then price.
curl "http://31.97.228.132:8000/api/elec/products?category=mobiles&brand=samsung&limit=1"
{
"total": 6,
"products": [
{
"product_id": 551,
"brand": "Samsung",
"category": "mobiles",
"display_name": "Samsung Galaxy A56 (8GB RAM, 256GB)",
"ram_gb": 8.0,
"storage_gb": 256.0,
"best_price": "42999.00",
"best_price_site": "Reliance Digital",
"platform_count": 5,
"image_url": "https://img-prd-pim.poorvika.com/product/Samsung-Galaxy-A56-5G-Awesome-Graphite-Main.png"
}
]
}
total is the full match count; use limit/offset to page. Some extra fields (brand_slug,
family, model, processor, sold_by_tn_retailer, updated_at, ...) are also present.
GET /api/elec/products/{product_id}
curl http://31.97.228.132:8000/api/elec/products/551
Everything from the list item, plus:
| Field | Content |
|---|---|
offers[] |
One per platform listing: site, domain, site_region (TN / national), price, mrp, in_stock, source_url, source_type, listing_title, observed_at, price_outlier |
canonical_specs |
Normalised specs, e.g. ram_gb, storage_gb, display_inch, processor, battery_mah, os |
spec_sources |
Per spec, the page it was read from |
images[] |
url, site, found_on (page the image is on), source_type |
rating |
{ value, count, sources: [{ site, rating, review_count, source_url }] }, or null |
reviews[] |
Up to 10 real customer reviews: site, source_url, author, rating, title, body, review_date, sentiment (positive / neutral / negative, from the reviewer's own stars) |
One offer from the response:
{
"site": "Reliance Digital",
"domain": "reliancedigital.in",
"site_region": "national",
"price": "42999.00",
"mrp": null,
"in_stock": true,
"source_url": "https://www.reliancedigital.in/product/samsung-galaxy-a56-5g-256-gb-8-gb-ram-awesome-olive-mobile-phone-m7x9g6-8968988",
"source_type": "scraped_page",
"observed_at": "2026-09-29T12:54:38.447374+05:30",
"price_outlier": false
}
Notes:
source_typesays how a value was read:scraped_page(the platform's product page),brand_official(the brand's site) orsearch_snippet(a search-engine result; Amazon and Flipkart are only read this way).price_outlier: truemarks a search-engine price that disagrees with the product-page prices. It is kept for transparency but never used asbest_price.in_stockistrue,falseornull(not stated).best_priceprefers in-stock offers, but a product that is out of stock everywhere still shows its price.- Reviews are often empty: most retailers do not publish review text in a readable form, and none are invented.
GET /api/elec/products/{product_id}/price-history
curl http://31.97.228.132:8000/api/elec/products/551/price-history
[
{ "site": "Reliance Digital", "price": "42999.00", "mrp": null, "in_stock": true,
"source_type": "scraped_page", "observed_at": "2026-09-29T12:54:38.447374+05:30" }
]
Oldest first, one row per observation per platform.
GET /api/elec/sites
curl http://31.97.228.132:8000/api/elec/sites
Each platform: name, domain, kind (marketplace, national_chain, tn_regional,
brand_official), region, and how many listings were read from it.
Using it from code
Python (requests)
import requests
from decimal import Decimal
BASE = "http://31.97.228.132:8000"
r = requests.get(f"{BASE}/api/elec/products",
params={"category": "laptops", "max_price": 60000, "limit": 100}, timeout=30)
r.raise_for_status()
for p in r.json()["products"]:
print(p["display_name"], Decimal(p["best_price"]), "at", p["best_price_site"])
detail = requests.get(f"{BASE}/api/elec/products/551", timeout=30).json()
for offer in detail["offers"]:
print(offer["site"], offer["price"], offer["source_url"])
JavaScript (fetch)
const BASE = "http://31.97.228.132:8000";
const res = await fetch(`${BASE}/api/elec/products?category=mobiles&q=galaxy`);
const { total, products } = await res.json();
products.forEach(p => console.log(p.display_name, p.best_price, p.best_price_site));
Browsers only allow calls from the web apps on the API's allowlist
(app.nearledaily.com, catalogue.nearle.ai.in, localhost:3100). Calls from servers, scripts,
curl and MCP clients are not affected. Ask for a new web origin to be added if needed.
MCP (for AI assistants)
The same catalogue is an MCP server (Streamable HTTP transport) at
http://31.97.228.132:8000/mcp/. Read-only, no login.
| Tool | Arguments | Returns |
|---|---|---|
list_categories |
none | Categories with product counts |
search_products |
query, category, brand, max_price, min_price, limit (all optional, limit 1–100, default 20) |
total and products: id, name, RAM/storage, best price and platform, platform count, image URL |
get_product |
product_id |
Offers per platform, specs, image_urls, rating, reviews |
price_history |
product_id |
Every observed price per platform |
Images are returned as URLs on the retailers' own image servers; nothing is re-hosted. Only part of the catalogue has an image.
Claude Code
claude mcp add --transport http electronics-catalog http://31.97.228.132:8000/mcp/
Cursor (.cursor/mcp.json) and other clients that take a URL
{ "mcpServers": { "electronics-catalog": { "url": "http://31.97.228.132:8000/mcp/" } } }
Python (fastmcp)
import asyncio
from fastmcp import Client
async def main():
async with Client("http://31.97.228.132:8000/mcp/") as c:
found = await c.call_tool("search_products", {"category": "laptops", "max_price": 60000})
print(found.data["total"])
detail = await c.call_tool("get_product", {"product_id": 551})
print(detail.data["display_name"], detail.data["image_urls"])
asyncio.run(main())
Example questions once connected: "Which laptops under ₹60,000 are sold on the most platforms?", "Compare Galaxy A56 prices across stores", "Show the price history of product 551."
Not part of this API
/api/auth/* and /api/elec/admin/* (collection runs, platform probes) need an admin login and are
for the catalogue operators only.