diff --git a/src/business-logic/import-pipeline/sources/dirtstreet/brands.js b/src/business-logic/import-pipeline/sources/dirtstreet/brands.js
new file mode 100644
index 0000000..73a978f
--- /dev/null
+++ b/src/business-logic/import-pipeline/sources/dirtstreet/brands.js
@@ -0,0 +1,14 @@
+/**
+ * Dirtstreet.in brand list — production config.
+ * Source: https://dirtstreet.in/shop-by-brands/
+ */
+
+const BRANDS = [
+ { name: "SC Project", slug: "scproject", brandUrl: "https://dirtstreet.in/brand/scproject/" },
+ { name: "Evotech Performance", slug: "evotechperformance", brandUrl: "https://dirtstreet.in/brand/evotechperformance/" },
+ { name: "DNA Air Filters", slug: "dnaairfilters", brandUrl: "https://dirtstreet.in/brand/dnaairfilters/" },
+ { name: "WRS", slug: "wrs", brandUrl: "https://dirtstreet.in/brand/wrs/" },
+ { name: "Zero Gravity Racing", slug: "zerogravityracing", brandUrl: "https://dirtstreet.in/brand/zerogravityracing/" },
+];
+
+module.exports = { BRANDS };
diff --git a/src/business-logic/import-pipeline/sources/dirtstreet/converter.js b/src/business-logic/import-pipeline/sources/dirtstreet/converter.js
new file mode 100644
index 0000000..e99de09
--- /dev/null
+++ b/src/business-logic/import-pipeline/sources/dirtstreet/converter.js
@@ -0,0 +1,143 @@
+const path = require("node:path");
+
+function slugify(str) {
+ return String(str || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
+}
+
+function getUploadedImageUrl(imgPath, uploadedImageMap) {
+ if (!uploadedImageMap || typeof uploadedImageMap !== "object") return null;
+ const bySourcePath = uploadedImageMap.bySourcePath || {};
+ return bySourcePath[String(imgPath)]?.url || null;
+}
+
+function getImageFileName(imagePath) {
+ try { return path.basename(new URL(imagePath).pathname); } catch { return path.basename(String(imagePath || "").split(/[?#]/)[0]); }
+}
+
+function availabilityToStockStatus(availability) {
+ if (!availability) return null;
+ const url = String(availability).toLowerCase();
+ if (url.includes("instock")) return "instock";
+ if (url.includes("outofstock")) return "outofstock";
+ if (url.includes("backorder") || url.includes("preorder")) return "onbackorder";
+ return null;
+}
+
+function buildDescriptionHtml(record) {
+ const parts = [];
+
+ if (record.descriptionHtml) {
+ parts.push(record.descriptionHtml);
+ } else if (record.description) {
+ parts.push(`
${record.description.replace(/\n/g, "
")}
`);
+ }
+
+ if (record.shortDescription && !parts.join("").includes(record.shortDescription)) {
+ parts.unshift(`${record.shortDescription}
`);
+ }
+
+ if (Array.isArray(record.attributes) && record.attributes.length) {
+ const rows = record.attributes
+ .map((attr) => `| ${attr.name} | ${attr.value} |
`)
+ .join("");
+ parts.push(``);
+ }
+
+ return parts.join("\n");
+}
+
+function convertDirtstreetRecordToShopifyReady(record, options = {}) {
+ const brand = options.brand || record.brand || "Dirtstreet";
+ const uploadedImageMap = options.uploadedImageMap || null;
+
+ const title = record.title || "Untitled Product";
+ const sku = record.sku || slugify(title);
+ const productId = sku || slugify(`${brand}-${title}`);
+ const price = Number(record.price ?? 0);
+ const compareAtPrice = record.compareAtPrice ? Number(record.compareAtPrice) : null;
+ const imagePaths = Array.isArray(record.images) ? record.images : [];
+
+ // Stock: prefer schema availability, fall back to stockStatus
+ const stockStatus = availabilityToStockStatus(record.availability) || record.stockStatus || null;
+ const quantity = stockStatus === "instock" ? 1 : 0;
+
+ const descriptionHtml = buildDescriptionHtml(record);
+
+ const files = imagePaths
+ .map((imgPath) => ({
+ type: "Image",
+ url: getUploadedImageUrl(imgPath, uploadedImageMap) || imgPath,
+ media_content: getImageFileName(imgPath),
+ source_path: imgPath,
+ }))
+ .filter((f) => f.url);
+
+ // Categories become subcategory / collections
+ const categories = Array.isArray(record.categories) ? record.categories : [];
+ const [primaryCategory, ...subCategories] = categories;
+ const category = primaryCategory || "Motorcycle Parts";
+ const subcategory = subCategories.join(", ");
+
+ // Variants — dirtstreet products are typically simple (single variant)
+ const variants = [{ sku, price, compare_price: compareAtPrice, quantity, optionValues: [] }];
+
+ const tags = [
+ brand,
+ category,
+ ...categories,
+ sku,
+ record.stockStatus || "",
+ ...(Array.isArray(record.tags) ? record.tags : []),
+ ].filter(Boolean).map((t) => String(t).trim());
+
+ return {
+ id: productId,
+ source: {
+ productId,
+ sourceKey: "dirtstreet",
+ url: record.url || null,
+ image_paths: imagePaths,
+ },
+ attributes: {
+ product_name: title,
+ brand,
+ category,
+ subcategory,
+ part_number: sku,
+ mfr_part_number: record.mpn || sku,
+ price,
+ compare_price: compareAtPrice,
+ purchase_cost: null,
+ barcode: record.gtin || record.barcode || "",
+ price_group: null,
+ units_per_sku: null,
+ part_description: descriptionHtml,
+ descriptions: descriptionHtml ? [{ type: "Market Description", description: descriptionHtml }] : [],
+ files,
+ image_paths: imagePaths,
+ dimensions: [{ weight: 0 }],
+ total_quantity: quantity,
+ inventorydata: { inventory: { main: quantity } },
+ options: [],
+ variants,
+ fitmentTags: { make: [], model: [], year: [], drive: [], baseModel: [] },
+ tags,
+ handle: slugify(`${brand}-${title}-${productId}`),
+ source_url: record.url || null,
+ },
+ };
+}
+
+function convertDirtstreetJsonToShopifyProducts(input, options = {}) {
+ const records = Array.isArray(input?.products) ? input.products : [];
+ return records.map((record) => {
+ // Each record in aggregated JSON has raw scraped data in record.scraped
+ const scraped = record.scraped || record;
+ return convertDirtstreetRecordToShopifyReady(scraped, {
+ brand: options.brand || scraped.brand || record.brand,
+ uploadedImageMap: options.uploadedImageMap,
+ });
+ });
+}
+
+module.exports = { convertDirtstreetRecordToShopifyReady, convertDirtstreetJsonToShopifyProducts };
diff --git a/src/business-logic/import-pipeline/sources/dirtstreet/index.js b/src/business-logic/import-pipeline/sources/dirtstreet/index.js
new file mode 100644
index 0000000..0143a1c
--- /dev/null
+++ b/src/business-logic/import-pipeline/sources/dirtstreet/index.js
@@ -0,0 +1,135 @@
+const fs = require("node:fs/promises");
+const path = require("node:path");
+const { BRANDS } = require("./brands");
+const { scrapeDirtstreetBrand } = require("./scraper");
+const { convertDirtstreetJsonToShopifyProducts } = require("./converter");
+
+const sourceKey = "dirtstreet";
+const label = "Dirtstreet";
+const dataDir = path.join("data", "sources", sourceKey);
+
+function paths() {
+ return {
+ aggregatedJson: path.join(dataDir, "01_products_aggregated.json"),
+ historyJson: path.join(dataDir, "01_products_run_history.json"),
+ downloadedImagesDir: path.join(dataDir, "02_downloaded_product_images"),
+ watermarkState: path.join(dataDir, "03_watermark_state.json"),
+ imageUploadState: path.join(dataDir, "04_shopify_image_upload_state.json"),
+ uploadedMapJson: path.join(dataDir, "04_shopify_uploaded_images_map.json"),
+ shopifyReadyJson: path.join(dataDir, "05_shopify_products_ready.json"),
+ logsDir: path.join(dataDir, "99_run_logs"),
+ };
+}
+
+async function fetchWebsiteData({ paths: runPaths }) {
+ const brandsToScrape = process.env.DIRTSTREET_BRANDS
+ ? BRANDS.filter((b) => process.env.DIRTSTREET_BRANDS.split(",").map((s) => s.trim()).includes(b.slug))
+ : BRANDS;
+
+ const allProducts = [];
+ const runSummary = [];
+ const now = new Date().toISOString();
+
+ for (const brand of brandsToScrape) {
+ console.log(`[DIRTSTREET] Scraping brand: ${brand.name} (${brand.brandUrl})`);
+ const brandStartedAt = new Date().toISOString();
+
+ try {
+ const products = await scrapeDirtstreetBrand(brand);
+
+ for (const product of products) {
+ allProducts.push({
+ productId: product.sku || product.url,
+ sourceKey,
+ brand: product.brand,
+ brandSlug: product.brandSlug,
+ // productSummary.img is read by shared downloadImages + uploadImages utilities
+ productSummary: {
+ id: product.sku || product.url,
+ name: product.title,
+ img: product.images || [],
+ cost: { mrp: product.price || 0 },
+ },
+ scraped: product,
+ });
+ }
+
+ runSummary.push({
+ brand: brand.name,
+ slug: brand.slug,
+ startedAt: brandStartedAt,
+ completedAt: new Date().toISOString(),
+ totalProducts: products.length,
+ success: true,
+ });
+
+ console.log(`[DIRTSTREET] ${brand.name}: ${products.length} products scraped`);
+ } catch (err) {
+ console.log(`[DIRTSTREET] ${brand.name} failed: ${err.message}`);
+ runSummary.push({
+ brand: brand.name,
+ slug: brand.slug,
+ startedAt: brandStartedAt,
+ completedAt: new Date().toISOString(),
+ totalProducts: 0,
+ success: false,
+ error: err.message,
+ });
+ }
+ }
+
+ const analysis = {
+ timestamp: now,
+ totalBrands: brandsToScrape.length,
+ totalProductsUnique: allProducts.length,
+ detailSuccess: allProducts.filter((p) => !p.scraped?.scrapeError).length,
+ detailFailed: allProducts.filter((p) => p.scraped?.scrapeError).length,
+ runSummary,
+ };
+
+ const payload = {
+ generatedAt: now,
+ sourceKey,
+ sourceLabel: label,
+ analysis,
+ products: allProducts,
+ };
+
+ await fs.mkdir(path.dirname(path.resolve(process.cwd(), runPaths.aggregatedJson)), { recursive: true });
+ await fs.writeFile(path.resolve(process.cwd(), runPaths.aggregatedJson), JSON.stringify(payload, null, 2), "utf8");
+
+ let history = [];
+ try {
+ const raw = await fs.readFile(path.resolve(process.cwd(), runPaths.historyJson), "utf8");
+ history = JSON.parse(raw);
+ if (!Array.isArray(history)) history = [];
+ } catch { history = []; }
+ history.push(analysis);
+ await fs.writeFile(path.resolve(process.cwd(), runPaths.historyJson), JSON.stringify(history, null, 2), "utf8");
+
+ console.log(`[DIRTSTREET] Saved ${allProducts.length} products to ${runPaths.aggregatedJson}`);
+
+ return {
+ analysis,
+ outputJsonPath: path.resolve(process.cwd(), runPaths.aggregatedJson),
+ historyPath: path.resolve(process.cwd(), runPaths.historyJson),
+ };
+}
+
+function convertToShopifyProducts(input, options = {}) {
+ return convertDirtstreetJsonToShopifyProducts(input, {
+ brand: options.brand || label,
+ uploadedImageMap: options.uploadedImageMap,
+ });
+}
+
+module.exports = {
+ sourceKey,
+ label,
+ defaultBrand: "Dirtstreet",
+ defaultImageBaseUrl: "",
+ envImageBaseUrl: "DIRTSTREET_IMAGE_BASE_URL",
+ paths,
+ fetchWebsiteData,
+ convertToShopifyProducts,
+};
diff --git a/src/business-logic/import-pipeline/sources/dirtstreet/scraper.js b/src/business-logic/import-pipeline/sources/dirtstreet/scraper.js
new file mode 100644
index 0000000..82653ce
--- /dev/null
+++ b/src/business-logic/import-pipeline/sources/dirtstreet/scraper.js
@@ -0,0 +1,423 @@
+/**
+ * Dirtstreet.in scraper - plain fetch + JSON-LD extraction (no browser).
+ *
+ * Dirtstreet is a WooCommerce store. Product pages contain JSON-LD schema.org
+ * markup. We fetch HTML directly and extract all fields via regex.
+ *
+ * 1. Brand pages paginated at /brand/{slug}/page/N/
+ * 2. Product detail via HTML + JSON-LD
+ */
+
+const BASE_URL = "https://dirtstreet.in";
+const CONCURRENCY = 3;
+
+function sleep(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
+
+async function fetchHtml(url, attempt = 1) {
+ try {
+ const res = await fetch(url, {
+ headers: {
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "accept-language": "en-US,en;q=0.9",
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124 Safari/537.36",
+ "cache-control": "no-cache",
+ },
+ });
+
+ if (res.status === 429 || res.status >= 500) {
+ if (attempt <= 4) {
+ const wait = attempt * 2000;
+ console.log(`[RETRY] ${url} -> HTTP ${res.status}, retrying in ${wait}ms (attempt ${attempt}/4)`);
+ await sleep(wait);
+ return fetchHtml(url, attempt + 1);
+ }
+ throw new Error(`HTTP ${res.status} after retries`);
+ }
+
+ if (res.status === 404) return null;
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
+ return res.text();
+ } catch (err) {
+ if (attempt <= 3 && (err.code === "UND_ERR_CONNECT_TIMEOUT" || err.code === "ECONNRESET")) {
+ await sleep(attempt * 2000);
+ return fetchHtml(url, attempt + 1);
+ }
+ throw err;
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+function decodeEntities(str) {
+ return String(str || "")
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, '"')
+ .replace(/'/g, "'")
+ .replace(/×/g, "x")
+ .replace(/ /g, " ")
+ .replace(/(\d+);/g, (_, code) => String.fromCharCode(Number(code)));
+}
+
+function cleanText(value) {
+ return decodeEntities(String(value || ""))
+ .replace(/<[^>]+>/g, " ")
+ .replace(/\s+/g, " ")
+ .trim();
+}
+
+function toNumber(value) {
+ if (!value) return null;
+ const match = String(value).match(/([\d,]+\.?\d*)/);
+ if (!match) return null;
+ const n = Number.parseFloat(match[1].replace(/,/g, ""));
+ return Number.isNaN(n) ? null : n;
+}
+
+function extractPriceFromHtml(html) {
+ // data-price attribute (WooCommerce sets this for JS cart)
+ const dp = html.match(/data-price=["']([\d.]+)["']/);
+ if (dp) { const n = Number.parseFloat(dp[1]); if (n > 0) return n; }
+
+ // Pattern on dirtstreet: ₹198,000.00
+ // Collect ALL such values, filter out 0, take the first valid one (the product price)
+ const bdiPattern = /(?:₹|₹)<\/span>([\d,]+(?:\.\d+)?)<\/bdi>/gi;
+ let bdiMatch;
+ while ((bdiMatch = bdiPattern.exec(html)) !== null) {
+ const n = toNumber(bdiMatch[1]);
+ if (n > 0) return n;
+ }
+
+ // sale price block
+ const ins = html.match(/[\s\S]{0,300}?(?:₹|₹)<\/span>([\d,]+(?:\.\d+)?)<\/bdi>[\s\S]{0,300}?<\/ins>/i);
+ if (ins) { const n = toNumber(ins[1]); if (n > 0) return n; }
+
+ // Fallback: ₹ number anywhere
+ const entity = html.match(/₹[^>]*>([\d,]+(?:\.\d+)?)/);
+ if (entity) { const n = toNumber(entity[1]); if (n > 0) return n; }
+
+ return null;
+}
+
+function availabilityToStockStatus(availability) {
+ if (!availability) return null;
+ const url = String(availability).toLowerCase();
+ if (url.includes("instock")) return "instock";
+ if (url.includes("outofstock")) return "outofstock";
+ if (url.includes("backorder") || url.includes("preorder")) return "onbackorder";
+ return null;
+}
+
+// ---------------------------------------------------------------------------
+// JSON-LD extraction
+// ---------------------------------------------------------------------------
+
+function extractJsonLd(html) {
+ const results = [];
+ const regex = /