User inquiry → VPS scraping → Taobao/Pinduoduo/Baidu/1688 → PI → Reply
Quick Jump:
Here is how the complete product sourcing system works — from user chat to PI delivery:
1. User sends inquiry (text + image)
User types: "এই মাউসটার মতো দরকার" + uploads product photo
2. Save to ProductInquiry entity
Inquiry stored with text + image_url, status = "pending"
3. Email alert to admin
SendEmail integration notifies admin: "New product inquiry from [user]"
4. AI recognizes product from image
InvokeLLM with vision model analyzes image → extracts keywords (brand, model, category)
5. Admin opens inquiry dashboard
Admin sees user name, text, image, AI keywords, clicks "Search Products"
6. VPS scrapes Taobao/Pinduoduo/Baidu/1688
Base44 backend function calls VPS API → Playwright browser opens & extracts products
7. Results displayed on admin page
Product name, price, MOQ, seller link, image — shown in a clean list
8. Admin creates Proforma Invoice (PI)
Admin selects products, adds markup, shipping cost → generates PI
9. Reply to user in chat + email
PI sent to user via in-app chat message + email notification
Components we will build:
Server details:
IP: 115.29.238.14 · Region: Hangzhou (China mainland) · OS: Ubuntu 20.04 · 2 vCPU / 2GB RAM
1.1 — SSH into your server
Open Terminal (Mac/Linux) or PowerShell (Windows) and run:
ssh root@115.29.238.14
Enter the root password you set during ordering. You will see a prompt like root@iZbp1...:~#
Expected output:
root@115.29.238.14's password: ******** Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-generic x86_64) root@iZbp16bd840bjqfvwjnqxeZ:~#
1.2 — Update the system
apt update && apt upgrade -y
Expected output:
Hit:1 http://mirrors.cloud.aliyuncs.com/ubuntu focal InRelease Hit:2 http://mirrors.cloud.aliyuncs.com/ubuntu focal-updates InRelease Reading package lists... Done Building dependency tree Reading state information... Done ... Processing triggers for libc-bin (2.31-0ubuntu9.18) ... Processing triggers for systemd (245.4-4ubuntu3.18) ...
1.3 — Install Node.js 20 LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - apt install -y nodejs # Verify node -v # should show v20.x.x npm -v # should show 10.x.x
Expected output:
## Installing the NodeSource Node.js 20.x repo... ... Reading package lists... Done Setting up nodejs (20.x-1nodesource1) ... root@iZbp16bd840bjqfvwjnqxeZ:~# node -v v20.x.x root@iZbp16bd840bjqfvwjnqxeZ:~# npm -v 10.x.x
1.4 — Install Playwright + Chromium browser
# Install system dependencies for Chromium
apt install -y libnss3 libatk1.0-0 libatk-bridge2.0-0 \
libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 \
libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2
# Install Playwright globally
npm install -g playwright
# Download Chromium browser
npx playwright install chromium
# Test it works
node -e "const {chromium}=require('playwright'); (async()=>{const b=await chromium.launch();console.log('Browser works!');await b.close();})()"Expected output:
Reading package lists... Done libnss3 is already the newest version (3.x-1) ... added 1 package in 2s root@iZbp16bd840bjqfvwjnqxeZ:~# npx playwright install chromium Downloading Chromium 1xx.x.x (playwright build) from https://playwright.azureedge.net/builds/... Chromium 1xx.x.x downloaded root@iZbp16bd840bjqfvwjnqxeZ:~# node -e "..." Browser works!
1.5 — Install PM2 (keeps server running forever)
npm install -g pm2
Expected output:
added 1 package in 3s root@iZbp16bd840bjqfvwjnqxeZ:~#
Create a Node.js API server on the VPS that Base44 can call to search products.
2.1 — Create project directory & install packages
mkdir -p /root/scraper-api cd /root/scraper-api npm init -y npm install express cors playwright
Expected output:
root@iZbp16bd840bjqfvwjnqxeZ:~# mkdir -p /root/scraper-api
root@iZbp16bd840bjqfvwjnqxeZ:~# cd /root/scraper-api
root@iZbp16bd840bjqfvwjnqxeZ:/root/scraper-api# npm init -y
Wrote to /root/scraper-api/package.json:
{
"name": "scraper-api",
"version": "1.0.0",
"main": "server.js",
...
}
root@iZbp16bd840bjqfvwjnqxeZ:/root/scraper-api# npm install express cors playwright
added 80 packages, and audited 81 packages in 10s
found 0 vulnerabilities2.2 — Create the API server file
Copy this code and save as /root/scraper-api/server.js on the VPS using nano:
// Save as: /root/scraper-api/server.js
const express = require("express");
const cors = require("cors");
const { chromium } = require("playwright");
const app = express();
app.use(cors());
app.use(express.json());
// API key security — only your Base44 app can call this
const API_KEY = process.env.SCRAPER_API_KEY || "bd2china-secret-2026";
// ─── Taobao Search ───
app.post("/api/search/taobao", async (req, res) => {
const { keyword, api_key } = req.body;
if (api_key !== API_KEY) return res.status(401).json({ error: "Unauthorized" });
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
try {
const url = "https://s.taobao.com/search?q=" + encodeURIComponent(keyword);
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
await page.waitForTimeout(3000);
const products = await page.$$eval("[class*=Content]", items => {
return items.slice(0, 10).map(item => {
const titleEl = item.querySelector("[class*=Title] span, a[title]");
const priceEl = item.querySelector("[class*=Price]");
const linkEl = item.querySelector("a[href]");
const imgEl = item.querySelector("img[src]");
return {
title: (titleEl && titleEl.textContent && titleEl.textContent.trim()) || "",
price: (priceEl && priceEl.textContent && priceEl.textContent.trim()) || "",
link: linkEl ? linkEl.href : "",
image: imgEl ? imgEl.src : "",
platform: "taobao"
};
}).filter(p => p.title);
});
res.json({ success: true, keyword, count: products.length, products });
} catch (error) {
res.json({ success: false, error: error.message, products: [] });
} finally {
await browser.close();
}
});
// ─── Pinduoduo Search ───
app.post("/api/search/pinduoduo", async (req, res) => {
const { keyword, api_key } = req.body;
if (api_key !== API_KEY) return res.status(401).json({ error: "Unauthorized" });
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
try {
const url = "https://mobile.yangkeduo.com/proxy/api/search?q=" + encodeURIComponent(keyword);
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
await page.waitForTimeout(3000);
const products = await page.$$eval("div[class*=goods]", items => {
return items.slice(0, 10).map(item => ({
title: (item.querySelector("[class*=title]") || {}).textContent || "",
price: (item.querySelector("[class*=price]") || {}).textContent || "",
image: item.querySelector("img") ? item.querySelector("img").src : "",
link: item.querySelector("a") ? item.querySelector("a").href : "",
platform: "pinduoduo"
})).filter(p => p.title);
});
res.json({ success: true, keyword, count: products.length, products });
} catch (error) {
res.json({ success: false, error: error.message, products: [] });
} finally {
await browser.close();
}
});
// ─── 1688 (Alibaba) Search ───
app.post("/api/search/1688", async (req, res) => {
const { keyword, api_key } = req.body;
if (api_key !== API_KEY) return res.status(401).json({ error: "Unauthorized" });
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
try {
const url = "https://s.1688.com/selloffer/offer_search.htm?keywords=" + encodeURIComponent(keyword);
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
await page.waitForTimeout(3000);
const products = await page.$$eval("[class*=offer]", items => {
return items.slice(0, 10).map(item => ({
title: (item.querySelector("[class*=title], .title") || {}).textContent || "",
price: (item.querySelector("[class*=price], .price") || {}).textContent || "",
image: item.querySelector("img") ? item.querySelector("img").src : "",
link: item.querySelector("a") ? item.querySelector("a").href : "",
platform: "1688"
})).filter(p => p.title);
});
res.json({ success: true, keyword, count: products.length, products });
} catch (error) {
res.json({ success: false, error: error.message, products: [] });
} finally {
await browser.close();
}
});
// ─── Baidu Search (general product info) ───
app.post("/api/search/baidu", async (req, res) => {
const { keyword, api_key } = req.body;
if (api_key !== API_KEY) return res.status(401).json({ error: "Unauthorized" });
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
try {
const url = "https://www.baidu.com/s?wd=" + encodeURIComponent(keyword);
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 20000 });
const results = await page.$$eval(".result", items => {
return items.slice(0, 10).map(item => ({
title: (item.querySelector("h3 a") || {}).textContent || "",
link: item.querySelector("h3 a") ? item.querySelector("h3 a").href : "",
snippet: (item.querySelector("[class*=content], .c-abstract") || {}).textContent || "",
platform: "baidu"
})).filter(r => r.title);
});
res.json({ success: true, keyword, count: results.length, products: results });
} catch (error) {
res.json({ success: false, error: error.message, products: [] });
} finally {
await browser.close();
}
});
// ─── Search ALL platforms ───
app.post("/api/search/all", async (req, res) => {
const { keyword, api_key } = req.body;
if (api_key !== API_KEY) return res.status(401).json({ error: "Unauthorized" });
const platforms = ["taobao", "pinduoduo", "1688", "baidu"];
const allResults = {};
for (const platform of platforms) {
try {
const resp = await fetch("http://localhost:3000/api/search/" + platform, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ keyword, api_key })
});
allResults[platform] = await resp.json();
} catch (e) {
allResults[platform] = { success: false, error: e.message, products: [] };
}
}
res.json({ success: true, keyword, results: allResults });
});
// ─── Health check ───
app.get("/api/health", (req, res) => {
res.json({ status: "ok", server: "bd2china-scraper", time: new Date().toISOString() });
});
const PORT = 3000;
app.listen(PORT, "0.0.0.0", () => {
console.log("Scraper API running on port " + PORT);
});nano server.js and press Enterls -la server.js → should show the fileIf you see -bash: syntax error near unexpected token — you pasted JavaScript into the shell instead of nano. The terminal runs shell commands, not JavaScript. Open nano first, then paste inside it.
2.3 — Start the server with PM2
cd /root/scraper-api # Set API key as environment variable export SCRAPER_API_KEY="bd2china-secret-2026" # Start with PM2 (auto-restarts on crash) pm2 start server.js --name scraper-api # Save PM2 config (auto-start on reboot) pm2 save pm2 startup # Check it is running pm2 status curl http://localhost:3000/api/health
Expected output:
root@iZbp16bd840bjqfvwjnqxeZ:/root/scraper-api# pm2 start server.js --name scraper-api
[PM2] Starting /root/scraper-api/server.js in fork_mode (1 instance)
[PM2] Done.
+---+------------------+----------+---------+----------+
| id| name | mode | status | restarts |
+---+------------------+----------+---------+----------+
| 0 | scraper-api | fork | online | 0 |
+---+------------------+----------+---------+----------+
root@iZbp16bd840bjqfvwjnqxeZ:/root/scraper-api# pm2 save
[PM2] Saving current process list...
[PM2] Successfully saved in /root/.pm2/dump.pm2
root@iZbp16bd840bjqfvwjnqxeZ:/root/scraper-api# pm2 startup
[PM2] Init System found: systemd
...
[PM2] [v] Command successfully executed.
root@iZbp16bd840bjqfvwjnqxeZ:/root/scraper-api# pm2 status
+---+------------------+-----------+
| id| name | status |
+---+------------------+-----------+
| 0 | scraper-api | online |
+---+------------------+-----------+
root@iZbp16bd840bjqfvwjnqxeZ:/root/scraper-api# curl http://localhost:3000/api/health
{"status":"ok","server":"bd2china-scraper","time":"2026-07-08T..."}Security: Open port 3000 in Alibaba Cloud
Go to your ECS console → Security Group → Add Inbound Rule:
3000/30000.0.0.0/0http://115.29.238.14:3000 that can search Taobao, Pinduoduo, 1688, and Baidu.Run these commands on your VPS to confirm everything is working. Copy-paste each one and check the output matches.
Run these on your VPS:
# ===== 1. Check Node.js =====
node -v
# Expected: v20.x.x
# ===== 2. Check npm =====
npm -v
# Expected: 10.x.x
# ===== 3. Check PM2 is installed =====
pm2 -v
# Expected: 5.x.x
# ===== 4. Check scraper-api is running =====
pm2 status
# Expected: scraper-api -> online
# ===== 5. Check Playwright + Chromium =====
npx playwright --version
# Expected: Version 1.xx.x
# ===== 6. Check server health endpoint =====
curl http://localhost:3000/api/health
# Expected: {"status":"ok","server":"bd2china-scraper",...}
# ===== 7. Check port 3000 is listening =====
ss -tlnp | grep 3000
# Expected: LISTEN 0.0.0.0:3000 (node server.js)
# ===== 8. Check from OUTSIDE the VPS (run on your laptop) =====
curl http://115.29.238.14:3000/api/health
# Expected: {"status":"ok",...}
# If this fails -> security group port 3000 not open yetExpected output (all green = you are good):
root@iZbp16bd840bjqfvwjnqxeZ:~# node -v
v20.x.x
root@iZbp16bd840bjqfvwjnqxeZ:~# npm -v
10.x.x
root@iZbp16bd840bjqfvwjnqxeZ:~# pm2 -v
5.x.x
root@iZbp16bd840bjqfvwjnqxeZ:~# pm2 status
+---+------------------+-----------+
| id| name | status |
+---+------------------+-----------+
| 0 | scraper-api | online |
+---+------------------+-----------+
root@iZbp16bd840bjqfvwjnqxeZ:~# npx playwright --version
Version 1.xx.x
root@iZbp16bd840bjqfvwjnqxeZ:~# curl http://localhost:3000/api/health
{"status":"ok","server":"bd2china-scraper","time":"2026-07-08T..."}
root@iZbp16bd840bjqfvwjnqxeZ:~# ss -tlnp | grep 3000
LISTEN 0 0 0.0.0.0:3000 0.0.0.0:* users:(("node",pid=12345,fd=18))If all commands return the expected output → your VPS is 100% ready!
If any check fails, go back to the step that installed it and re-run the commands.
If step 8 (outside curl) fails:
Your server works locally but is blocked by Alibaba Cloud's security group. Go to:
ECS Console → Instances → Security Groups → Add Inbound Rule → Port 3000 → Source 0.0.0.0/0 → Allow
Store the VPS IP and API key as secrets so backend functions can call the VPS securely.
SCRAPER_VPS_IP115.29.238.14SCRAPER_VPS_PORT3000SCRAPER_API_KEYbd2china-secret-2026Tell the chat: "Set secrets: SCRAPER_VPS_IP, SCRAPER_VPS_PORT, SCRAPER_API_KEY"
The AI will create a secrets form for you to fill in securely.
This function sits in Base44 and calls the VPS API when admin searches for products.
// File: base44/functions/scrapeProducts/entry.ts
import { createClientFromRequest } from "npm:@base44/sdk@0.8.31";
Deno.serve(async (req) => {
try {
const base44 = createClientFromRequest(req);
const user = await base44.auth.me();
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
const body = await req.json();
const keyword = body.keyword;
const platform = body.platform || "all";
const inquiryId = body.inquiry_id;
const VPS_IP = Deno.env.get("SCRAPER_VPS_IP");
const VPS_PORT = Deno.env.get("SCRAPER_VPS_PORT") || "3000";
const API_KEY = Deno.env.get("SCRAPER_API_KEY");
const vpsUrl = "http://" + VPS_IP + ":" + VPS_PORT + "/api/search/" + platform;
const vpsResponse = await fetch(vpsUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ keyword, api_key: API_KEY })
});
const data = await vpsResponse.json();
if (inquiryId && data.success) {
await base44.asServiceRole.entities.ProductInquiry.update(inquiryId, {
search_results: JSON.stringify(data.results || data.products || []),
status: "searched"
});
}
return Response.json({ success: true, data });
} catch (error) {
return Response.json({ error: error.message }, { status: 500 });
}
});Ask the AI builder: "Create the scrapeProducts backend function" after setting the secrets.
A database table to store every product inquiry from users.
{
"name": "ProductInquiry",
"type": "object",
"properties": {
"user_id": { "type": "string" },
"user_name": { "type": "string" },
"user_email": { "type": "string" },
"user_phone": { "type": "string" },
"image_url": { "type": "string" },
"ai_identified_keywords": { "type": "string" },
"ai_product_name": { "type": "string" },
"product_keyword": { "type": "string" },
"product_description": { "type": "string" },
"quantity_needed": { "type": "integer" },
"target_price": { "type": "number" },
"platforms_searched": {
"type": "array",
"items": { "type": "string", "enum": ["taobao","pinduoduo","1688","baidu"] }
},
"search_results": { "type": "string" },
"selected_products": { "type": "string" },
"pi_data": {
"type": "object",
"properties": {
"pi_number": { "type": "string" },
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"price": { "type": "number" },
"quantity": { "type": "integer" },
"total": { "type": "number" }
}
}
},
"subtotal": { "type": "number" },
"shipping_cost": { "type": "number" },
"total": { "type": "number" },
"currency": { "type": "string", "default": "CNY" },
"valid_until": { "type": "string" }
}
},
"status": {
"type": "string",
"enum": ["pending","searched","pi_created","replied","closed"],
"default": "pending"
},
"admin_notes": { "type": "string" },
"reply_message": { "type": "string" }
},
"required": ["user_id", "product_keyword"]
}Status flow:
pending → searched → pi_created → replied → closed
When a user sends a product request, it gets saved to the database AND emails the admin.
এই মাউসটার মতো দরকার, ৫০০ পিস। দাম ১৫ ইউয়ানের নিচে হলে ভালো।
আপনার অনুরোধ গ্রহণ করা হয়েছে! আমরা Taobao/1688 এ খুঁজছি। ফলাফল কিছুক্ষণের মধ্যে পাবেন।
When a user uploads a product image, the system uses LLM vision to identify the product and search automatically.
AI Image Analysis...
Detected: Wireless Mouse, 2.4GHz, Black, Ergonomic Design
Chinese keyword: 无线鼠标 2.4G 黑色
// File: base44/functions/identifyProductFromImage/entry.ts
import { createClientFromRequest } from "npm:@base44/sdk@0.8.31";
Deno.serve(async (req) => {
try {
const base44 = createClientFromRequest(req);
const user = await base44.auth.me();
if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });
const body = await req.json();
const { inquiry_id, image_url, user_text } = body;
// Step 1: Use LLM vision to identify the product from the image
const identification = await base44.asServiceRole.integrations.Core.InvokeLLM({
prompt: "You are a product identification expert. Look at this product image and identify:\n" +
"1. Product name (in English)\n" +
"2. Chinese name (for Taobao/1688 search)\n" +
"3. Brand (if visible)\n" +
"4. Category\n" +
"5. Key features (color, size, material, model number if visible)\n" +
"6. Best search keyword for Chinese e-commerce\n\n" +
"User also said: " + (user_text || "(no text)"),
file_urls: [image_url],
response_json_schema: {
type: "object",
properties: {
product_name_en: { type: "string" },
product_name_cn: { type: "string" },
brand: { type: "string" },
category: { type: "string" },
key_features: { type: "string" },
search_keyword: { type: "string" },
search_keyword_cn: { type: "string" }
}
}
});
// Step 2: Save results to the inquiry
if (inquiry_id) {
await base44.asServiceRole.entities.ProductInquiry.update(inquiry_id, {
ai_product_name: identification.product_name_en || "",
ai_identified_keywords: JSON.stringify(identification),
product_keyword: identification.search_keyword_cn || identification.search_keyword || identification.product_name_en || "",
});
}
return Response.json({
success: true,
identification,
search_keyword: identification.search_keyword_cn || identification.search_keyword || identification.product_name_en
});
} catch (error) {
return Response.json({ error: error.message }, { status: 500 });
}
});Why this works:
The InvokeLLM integration supports file_urls — pass the uploaded image URL and the LLM vision model will analyze it. The prompt asks for Chinese keywords so the VPS scraper can search Taobao/1688 in Chinese for better results.
Admin opens the inquiry, clicks "Search", and sees results from all 4 platforms.
User: Anwar Hossain (anwar@gmail.com)
Product: wireless mouse
Qty: 500 · Target price: ¥15/pc
Wireless Mouse 2.4G Silent Click
Shenzhen Tech Co
¥8.50
Bulk Wireless Mouse MOQ 100
Guangzhou Wholesale
¥6.20
Office Wireless Mouse Slim
Yiwu Trading
¥12.90
Admin selects products from search results and builds a Proforma Invoice.
| Product | Qty | Unit Price | Total |
|---|---|---|---|
| Wireless Mouse 2.4G (Taobao) | 500 | ¥8.50 | ¥4,250 |
| Shipping (BD → Dhaka, Air Cargo) | — | — | ¥800 |
| Grand Total | ¥5,050 | ||
PI is sent to the user both in-app chat and via email.
PI-2026-001 তৈরি হয়েছে!
আপনার অনুরোধ অনুযায়ী ৫০০ পিস wireless mouse খুঁজে পেয়েছি।
Wireless Mouse 2.4G × 500
একক দাম: ¥8.50
শিপিং: ¥800 (Air Cargo, 7-10 দিন)
মোট: ¥5,050 (~৳92,000)
PI টি ৭ দিন পর্যন্ত বৈধ। অর্ডার কনফার্ম করতে রিপ্লাই দিন।
Tell the chat what you want to build next: