A negative review on Ozon or Wildberries can go unanswered for 24 hours. In that time, it can deter dozens of potential buyers. We developed a bot that scans reviews in real-time and instantly notifies your team via Telegram. According to our data, 70% of buyers change their opinion of a brand after a quick response to a negative review. Our bot cuts reaction time from hours to minutes – a 97% reduction. Over the years, we've completed 30+ projects in reputation monitoring — from simple parsers to enterprise systems.
Why the Bot Pays Off in a Month
Manual checking of 1000 product cards across multiple marketplaces can take up to 4 hours daily. The bot does the same work in minutes and costs a one-time investment that pays off in 1–2 months through reduced labor costs and increased conversion. In one project for a large electronics seller, we replaced manual monitoring of 500 cards with automation. Reaction time to negative reviews dropped from 4 hours to 2 minutes (97% faster), and conversion to loyalty increased by 15%. With 10,000 reviews processed per month, the bot saves 200+ hours of manual work.
Bot Faster Than Manual Monitoring
Manual checking of dozens of product cards on multiple platforms takes hours. The bot checks each platform on a schedule: for API platforms — once per hour, for parsing — once every 4 hours. Notifications arrive in Telegram within 1–2 minutes of a review appearing. This is 10–20 times faster than manual approach.
| Parameter | Manual | Automated |
|---|---|---|
| Reaction time to negative | 2–24 hours | 1–5 minutes |
| Number of cards checked per day | up to 50 | unlimited |
| Check frequency | 1 time per day | every hour |
How to Set Up the Bot in 3 Steps
Step 1: Prepare sources. You provide a list of product URLs or API keys (for Yandex Market). We create records in review_sources.
Step 2: Run adapters. The bot executes reviews:check --platform=... commands on schedule. Each adapter implements the ReviewAdapterInterface and returns an array of ReviewDTO.
Step 3: Configure notifications. Specify a Telegram chat for regular reviews and a separate chat for urgent (negative) ones. Everything is ready to go.
Architecture of Platform Adapters
Each platform is a separate adapter. For platforms with an API (Yandex Market), we use direct requests; for others, we parse HTML via Playwright. All adapters implement a unified ReviewAdapterInterface, making it easy to add new platforms. The system uses an asynchronous queue with Redis for job management and Supervisor for process monitoring.
The data schema includes two main tables: review_sources (product links) and reviews (the reviews themselves). Indexes speed up selection of unread reviews. Here is the structure:
CREATE TABLE review_sources (
id BIGSERIAL PRIMARY KEY,
platform VARCHAR(50) NOT NULL, -- 'yandex_market', 'ozon', 'google', 'otzovik'
product_id BIGINT REFERENCES products(id),
external_url TEXT NOT NULL,
external_id VARCHAR(255), -- ID карточки на площадке
scrape_config JSONB,
is_active BOOLEAN DEFAULT TRUE,
UNIQUE(platform, external_url)
);
CREATE TABLE reviews (
id BIGSERIAL PRIMARY KEY,
source_id BIGINT REFERENCES review_sources(id),
external_id VARCHAR(255), -- ID отзыва на площадке
author VARCHAR(255),
rating SMALLINT, -- 1–5
text TEXT,
pros TEXT,
cons TEXT,
published_at TIMESTAMP,
discovered_at TIMESTAMP DEFAULT NOW(),
sentiment VARCHAR(20), -- 'positive', 'negative', 'neutral' (ML)
is_notified BOOLEAN DEFAULT FALSE,
UNIQUE(source_id, external_id)
);
CREATE INDEX idx_reviews_rating ON reviews(source_id, rating);
CREATE INDEX idx_reviews_notified ON reviews(source_id) WHERE is_notified = FALSE;
API or Parsing: Comparison
API is more stable, but not all platforms open it. Yandex Market provides an API to partners — no blocks and full data. Ozon and Wildberries do not offer an API for reviews, so we use Playwright: the browser renders the page and extracts reviews. It's slower but reliable. We implement exponential backoff and retry logic to handle transient failures.
Here's a comparison:
| Characteristic | API (Yandex Market) | Parsing (Ozon/WB) |
|---|---|---|
| Stability | High | Medium (depends on page structure) |
| Speed | Instant | 2–5 seconds per page |
| Data completeness | All fields | Limited by HTML |
| Blocking risk | None | Minimal with reasonable delays (3–5 sec between requests) |
Adapters for Yandex Market and Ozon:
class YandexMarketReviewAdapter implements ReviewAdapterInterface
{
public function fetchReviews(ReviewSource $source): array
{
$modelId = $source->external_id;
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . config('services.yandex_market.token'),
])->get("https://api.partner.market.yandex.ru/v2/models/{$modelId}/reviews", [
'count' => 30,
'page' => 1,
]);
return collect($response->json('result.reviews', []))
->map(fn($r) => new ReviewDTO(
externalId: (string) $r['id'],
author: $r['author']['name'] ?? 'Аноним',
rating: (int) $r['grade'],
text: $r['text'] ?? '',
pros: $r['pros'] ?? null,
cons: $r['cons'] ?? null,
publishedAt: Carbon::parse($r['date']),
))
->toArray();
}
}
class OzonReviewAdapter implements ReviewAdapterInterface
{
public function fetchReviews(ReviewSource $source): array
{
$data = $this->playwright->evaluate($source->external_url, <<<JS
await page.waitForSelector('[data-widget="webReviewProductScore"]', {timeout: 10000});
const items = document.querySelectorAll('[data-widget="webSingleReview"]');
return Array.from(items).map(el => ({
id: el.dataset.reviewId,
rating: parseInt(el.querySelector('[data-rating]')?.dataset.rating) || 0,
text: el.querySelector('.review-text')?.textContent?.trim() || '',
pros: el.querySelector('.pros')?.textContent?.trim() || null,
cons: el.querySelector('.cons')?.textContent?.trim() || null,
author: el.querySelector('.author-name')?.textContent?.trim() || 'Аноним',
date: el.querySelector('time')?.getAttribute('datetime'),
}));
JS);
return collect($data)->map(fn($r) => new ReviewDTO(
externalId: $r['id'],
author: $r['author'],
rating: $r['rating'],
text: $r['text'],
pros: $r['pros'],
cons: $r['cons'],
publishedAt: $r['date'] ? Carbon::parse($r['date']) : now(),
))->toArray();
}
}
Sentiment Analysis Mechanism
We use two levels. If the rating is 1–2 or 4–5, sentiment is obvious. For a rating of 3, we trigger keyword analysis or AI:
class SentimentAnalyzer
{
private array $negativeKeywords = [
'брак', 'сломан', 'не работает', 'возврат', 'обман',
'разочарован', 'ужас', 'кошмар', 'мусор', 'дрянь',
];
private array $positiveKeywords = [
'отлично', 'супер', 'доволен', 'рекомендую', 'превзошёл',
'быстро', 'качественно', 'спасибо',
];
public function analyze(ReviewDTO $review): string
{
if ($review->rating <= 2) return 'negative';
if ($review->rating >= 4) return 'positive';
$text = mb_strtolower($review->text . ' ' . $review->cons);
foreach ($this->negativeKeywords as $kw) {
if (str_contains($text, $kw)) return 'negative';
}
return 'neutral';
}
public function analyzeWithAI(string $text): string
{
$response = $this->openai->chat()->create([
'model' => 'gpt-4o-mini',
'messages' => [
['role' => 'system', 'content' => 'Определи тональность отзыва. Ответь одним словом: positive, negative или neutral.'],
['role' => 'user', 'content' => $text],
],
'max_tokens' => 10,
]);
return in_array($response->choices[0]->message->content, ['positive', 'negative', 'neutral'])
? $response->choices[0]->message->content
: 'neutral';
}
}
Notifications are sent to Telegram: for negative reviews — to a separate urgent support chat, for others — to the general chat. The message includes rating, review text, and a link.
class ReviewNotifier
{
public function notifyNew(Review $review): void
{
$emoji = match ($review->sentiment) {
'positive' => '⭐',
'negative' => '🚨',
default => '💬',
};
$stars = str_repeat('★', $review->rating) . str_repeat('☆', 5 - $review->rating);
$text = "{$emoji} *Новый отзыв* — {$review->source->platform}\n"
. "{$stars} {$review->rating}/5\n"
. "*{$review->author}*\n\n"
. mb_substr($review->text, 0, 300)
. (mb_strlen($review->text) > 300 ? '...' : '') . "\n\n"
. "[Открыть отзыв]({$review->source->external_url})";
$chatId = $review->sentiment === 'negative'
? config('telegram.urgent_reviews_chat')
: config('telegram.reviews_chat');
$this->telegram->sendMessage([
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'Markdown',
]);
$review->update(['is_notified' => true]);
}
}
Rating analytics and check schedule:
-- Агрегат рейтинга по платформам за последние 30 дней
SELECT
rs.platform,
COUNT(*) AS total_reviews,
ROUND(AVG(r.rating), 2) AS avg_rating,
COUNT(*) FILTER (WHERE r.rating <= 2) AS negative_count,
COUNT(*) FILTER (WHERE r.rating >= 4) AS positive_count
FROM reviews r
JOIN review_sources rs ON r.source_id = rs.id
WHERE r.published_at >= NOW() - INTERVAL '30 days'
GROUP BY rs.platform
ORDER BY avg_rating;
-- Быстрые платформы с API — каждый час
$schedule->command('reviews:check --platform=yandex_market')->hourly();
-- Парсинг через браузер — каждые 4 часа (ресурсоёмко)
$schedule->command('reviews:check --platform=ozon')->everyFourHours();
$schedule->command('reviews:check --platform=wildberries')->everyFourHours();
-- Еженедельный сводный отчёт с трендом рейтинга
$schedule->job(new WeeklyReviewsReportJob)->weekly()->mondays()->at('09:00');
What If the Platform Changes Its HTML Structure?
Parsing depends on the layout. When selectors change, the adapter stops working. We've built in monitoring: once a day, the bot checks that the adapter returns a non-empty result. If there's an error — notification via Telegram. We update selectors promptly. Our contract includes 2 weeks of post-release support during which adapters are adjusted for free.
What's Included in the Work?
The turnkey project includes: data schema design, development of adapters for three platforms, schedule configuration, sentiment analysis implementation, Telegram notifications, analytics dashboard in the admin panel. Timeline: basic version — 2 days, full package — up to 5 business days. Cost is calculated individually based on your stack and number of platforms. We guarantee stable bot operation as long as platform limits are respected.
Contact us — we'll evaluate your project in one business day. Get a consultation from an engineer.







