Repricing Bot Development for Marketplaces
You wake up in the morning and see your product on Ozon has dropped to page 100 of search results — competitors undercut your price overnight. Handmade dumping leads to zero margin and unsold stock. You need automation: a bot that monitors competitor prices, applies flexible strategies, and publishes adjustments via marketplace APIs. Below is how we do it.
When Manual Management Doesn't Suffice: Typical Seller Problems
Competitors change prices several times a day — it's impossible to keep up manually: either you miss profit or sell at a loss. Many Excel-based strategies fail because response time is measured in minutes, not hours. There is no protection against price wars — two sellers drive the price to zero while a third one dumps at cost. Testing hypotheses is difficult — a rule like "reduce by 5% if a competitor reduces" without a repricing bot turns into an analog nightmare.
A properly configured repricing system ensures stable conversion growth while preserving margins. One wrong coefficient and you are in the red. Therefore the bot must: enforce a margin floor, analyze Buy Box position, and forcibly pause when a price war is suspected.
How We Build the Repricing System
Our tech stack: PHP 8.3 + Laravel 11 for the backend, PostgreSQL for storing rules, logs, and statistics. The repricing engine consists of clearly separated classes: RepricingRule, RepricingEngine, PriceWarDetector, and OzonPricePublisher / WbPricePublisher.
Repricing Rule Data Schema
CREATE TABLE repricing_rules (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
marketplace VARCHAR(50) NOT NULL, -- 'ozon', 'wildberries', 'yandex_market'
scope_type VARCHAR(20) NOT NULL, -- 'global', 'category', 'product'
scope_id BIGINT,
strategy VARCHAR(30) NOT NULL, -- 'min_price', 'buy_box', 'rule_based'
min_price_mode VARCHAR(20) DEFAULT 'margin_floor',
min_price_value NUMERIC(12,2),
min_margin_pct NUMERIC(5,2) DEFAULT 10,
max_price NUMERIC(12,2),
step_pct NUMERIC(5,2) DEFAULT 1.0,
step_abs NUMERIC(10,2),
cooldown_minutes INT DEFAULT 60,
is_active BOOLEAN DEFAULT TRUE
);
CREATE TABLE repricing_log (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT REFERENCES products(id),
marketplace VARCHAR(50),
old_price NUMERIC(12,2),
new_price NUMERIC(12,2),
reason TEXT,
rule_id BIGINT REFERENCES repricing_rules(id),
triggered_at TIMESTAMP DEFAULT NOW()
);
This structure allows flexible rule configuration: at the store level (global), category level, or for a specific product. Rules contain mandatory limits — minimum price (fixed or % of cost), maximum price, step size, and cooldown between publications. The log is used for debugging and analytics.
Getting Competitor Prices: Ozon and Wildberries
For Ozon, we use the /v1/product/info/competitor-price endpoint. It returns competitor prices in the Buy Box along with their rating. Example PHP call:
class OzonCompetitorPriceClient
{
public function getCompetitorPrices(string $offerId): array
{
$response = Http::withHeaders([
'Client-Id' => $this->clientId,
'Api-Key' => $this->apiKey,
])->post('https://api-seller.ozon.ru/v1/product/info/competitor-price', [
'offer_id' => $offerId,
]);
return $response->json('result', []);
}
}
For Wildberries, we access the product card and parse card.wb.ru (or use the API v2 in some cases). We get an array of sizes with prices for each size, including competitor prices.
The Repricing Engine: How Decisions Are Made
class RepricingEngine
{
public function calculateNewPrice(
Product $product,
string $marketplace,
RepricingRule $rule,
): ?PriceDecision {
$competitorData = $this->getCompetitorData($product, $marketplace);
$costPrice = $product->cost_price ?? 0;
$currentPrice = $this->getCurrentMarketplacePrice($product, $marketplace);
$decision = match ($rule->strategy) {
'min_price' => $this->strategyMinPrice($currentPrice, $competitorData, $rule, $costPrice),
'buy_box' => $this->strategyBuyBox($currentPrice, $competitorData, $rule, $costPrice),
'rule_based' => $this->strategyRuleBased($currentPrice, $competitorData, $rule, $costPrice),
default => null,
};
if (!$decision) return null;
// Check cooldown
$lastChange = RepricingLog::where('product_id', $product->id)
->where('marketplace', $marketplace)
->where('triggered_at', '>=', now()->subMinutes($rule->cooldown_minutes))
->exists();
if ($lastChange) return null;
return $decision;
}
private function strategyMinPrice(
float $current, array $competitors, RepricingRule $rule, float $costPrice
): ?PriceDecision {
$competitorMin = collect($competitors)->min('price');
if (!$competitorMin) return null;
$floor = $this->calculateFloor($rule, $costPrice);
if ($competitorMin < $current) {
$newPrice = max($competitorMin, $floor);
if ($newPrice >= $current) return null;
return new PriceDecision(
newPrice: $newPrice,
reason: "Competitor lowered price to {$competitorMin}",
);
}
if ($competitorMin > $current && $rule->max_price && $current < $rule->max_price) {
$newPrice = min($competitorMin - 1, $rule->max_price);
return new PriceDecision(
newPrice: $newPrice,
reason: "Competitor raised price to {$competitorMin}",
);
}
return null;
}
private function calculateFloor(RepricingRule $rule, float $costPrice): float
{
if ($rule->min_price_mode === 'fixed' && $rule->min_price_value) {
return $rule->min_price_value;
}
if ($rule->min_margin_pct && $costPrice > 0) {
return $costPrice * (1 + $rule->min_margin_pct / 100);
}
return 0;
}
}
Key strategy: Min Price. It compares the lowest competitor price with the current price: if the competitor is cheaper, we lower to that price (but not below the computed floor); if the competitor is more expensive and a ceiling is set, we raise. The cooldown prevents frequent oscillations.
Price War Protection
A price war occurs when two sellers continuously undercut each other. Our detector:
class PriceWarDetector
{
public function isWarring(int $productId, string $marketplace): bool
{
$changes = RepricingLog::where('product_id', $productId)
->where('marketplace', $marketplace)
->where('triggered_at', '>=', now()->subDay())
->count();
if ($changes >= 5) {
Cache::put("repricing.paused.{$productId}.{$marketplace}", true, now()->addHours(6));
Notification::send($this->admins, new PriceWarAlert($productId, $marketplace));
return true;
}
return false;
}
}
The threshold (5 changes in 24 hours) is configurable per product category. While the bot is frozen, you can manually review or increase the price.
Monitoring and Reports: SQL for Analytics
SELECT
p.name,
rl.marketplace,
COUNT(*) AS changes_count,
MIN(rl.new_price) AS min_price_today,
MAX(rl.new_price) AS max_price_today,
ROUND(AVG(rl.new_price), 2) AS avg_price_today
FROM repricing_log rl
JOIN products p ON p.id = rl.product_id
WHERE rl.triggered_at >= NOW() - INTERVAL '24 hours'
GROUP BY p.name, rl.marketplace
ORDER BY changes_count DESC;
This query gives a daily snapshot: which products changed frequently and their price range. You can add segmentation by strategy or category.
Task Scheduling
$schedule->job(new RunRepricingJob)->everyThirtyMinutes();
$schedule->job(new ResetRepricingCountersJob)->dailyAt('03:00');
Repricing runs every 30 minutes for active rules. Counters reset nightly. The timing can be adjusted to match peak load of the specific marketplace.
Comparison of Repricing Strategies
| Strategy | Description | When to Use |
|---|---|---|
| Min Price | Keep price at the lowest competitor level | High competition, low differentiation products |
| Buy Box | Target winning the Buy Box (price + rating) | Products where Buy Box drives 80% of sales |
| Rule-Based | Flexible conditions (e.g., reduce by 5% when competitor reduces) | Complex scenarios with seasonality or promotions |
| Margin Floor | Protect minimum margin (price >= cost + % margin) | Low-margin products |
What's Included in the Repricing Bot Development
We implement the project end-to-end in 6–8 business days. The scope includes:
- Data schema design and engine for core strategies (2 days)
- Ozon API integration (competitor prices + publishing) (1–2 days)
- Wildberries API integration (+1 day)
- PriceWarDetector mechanism with cooldown and alerts (1 day)
- Rule management interface and change log (1–2 days)
- Full API documentation (request/response schemas), rule configuration guide, team training for basic administration, and launch support.
We guarantee correct integration with official APIs — our experience includes over 50 marketplace automation projects. The price is fixed in the contract and does not change during development.
Checklist: Common Repricing Implementation Mistakes
- No cooldown — prices change every 5 minutes; Ozon blocks the API.
- Missing min_price — you sell at a loss.
- Forgetting about reversal — if price drops below cost, the bot must immediately stop adjusting.
- No error monitoring — silent fails when publishing a price leave the product at the old price.
Contact us to discuss your scenario — we'll send a sample technical specification and answer your questions within a day.







