Repricer
ENRU
Case study - pricing loop

The price stopped being a guess.

A skincare seller on a major e-commerce marketplace, with a catalog where two thirds of the items are sets and multipacks. Four tools pull the price in different directions: do not run out of stock, follow the median up, do not sell below the margin floor, do not lose to rivals. All four run through one engine, and every recommendation can explain itself.

FastAPIPostgreSQL 16CeleryReact 18 TypeScriptSeller APIPerformance APIFinance module
64
data tables across 13 backend modules
97
API endpoints in the portal
137
backend autotests, run every night
4
pricing tools inside one calculation pipeline
60 days
of price, cost and advertising history collected
0
write methods against the marketplace: advice only
01 Context and pain

Prices were moved by hand, and nobody saw the shelf price

The client changed prices manually in the seller account. The decision was made on the seller price, while the customer sees a different one: the marketplace adds a discount of its own. Comparison against rivals therefore ran on numbers that do not exist on the shelf.

The marketplace discount hides the real price

The seller price and the shelf price differ by almost half. While the interface showed only the seller price, a manager kept the conversion in their head and got it wrong, and any comparison with a rival was meaningless.

Stock ran out and the price did not notice

Three days of stock and two weeks of stock need different reaction speeds. A single "raise by N percent" rule is not enough for both: in the first case it is late, in the second it gets in the way.

Cost appeared in the table twice

A read-only column and an edit field elsewhere on the screen. A person picked between them at random, and the two drifted apart silently.

Sets and multipacks break the usual formulas

Two thirds of the catalog are bundles. Cost is tracked per whole item and never split by contents: that is how the client counts money, and the system has to count the same way.

02 Architecture

One calculation pipeline, four screens on top of it

The syncs collect the catalog, orders, finance and advertising. The finance module turns transactions into cost per unit. The pricing engine computes the recommendation. The repricer runs the same engine in read mode and lays the result out across four tabs: duplicating the logic on the screen is not allowed, otherwise there would be two prices and two answers to one question.

SOURCES · READ ONLY Seller API catalog · prices · stock postings · finance pagination and limits Performance API campaigns and spend per-order report only one active report Cost of goods xlsx template for every item history and author of an edit Syncs and finance Celery, a log per run the requested report id lives in Redis: after a crash it is downloaded, not reordered costs as a share of revenue, not roubles divided by pieces duplicates collapse before the write PostgreSQL 16 64 tables · 60 days of history Redis 7: cache, broker, report memory Pricing engine one entry point and one result per item proposals from every tool and the disputes between them the floor and the ceiling a recommendation, not a write Repricer Out of Stock · Median Margin control · Rivals account setting and item setting Daily pulse the everyday table the same price as the engine Alerts and feed a message per item: what the engine did and why THE KEY DECISION The repricer does not compute the price. It runs the same pipeline in read mode and lays the result out across tabs. Logic of its own would mean two prices and two answers to one question.
03 Four tools

Every tab answers for its own risk

TabWhat it showsWhat it configures
Out of Stock stock, order speed, how many days it lasts, the step of the raise ladder the target in days of stock and the step: shared for the account, own per item
Median price the 30 day median, the growth in percent, the price row as a chart, the raise rule minimum and maximum price, two comparison periods, the raise step
Margin control cost edited right in the cell, cost per unit, margin, advertising, ad share, promotions at a loss minimum margin, whether advertising counts, the window of actual costs
Rivals rival prices after the marketplace discount, how much dearer we are, when it was checked how much cheaper to be, whether to chase the price, how many rivals to take
The Out of Stock tab: stock, order speed, ladder step, current and suggested price as a pair
The ladder is read from the sharpest shortage to the mildest. Under every price there is a second line with the shelf price: under the current one and under the suggested one alike.

The account value is the default, the item value wins. An empty cell shows the inherited account value in grey, a filled one shows its own. Clearing a cell means going back to the shared value: the server receives empty, not zero. Empty and zero are different things here and must not be confused: a zero in the raise step would mean "never raise".

The "to all items" button does two things

It writes the value into the account settings and clears per-item values for that field. Otherwise the button would skip exactly the items somebody had tuned by hand, and would look broken.

A ladder instead of a single step

The list of rungs is read from the sharpest shortage to the mildest. One step for every case behaved the same at six days of stock and at one day, although the second case needs a different reaction speed.

04 The price floor

A conservative principle: max(actual; tariff)

When a minimum price is not set by hand, the floor comes from the required margin. That needs the cost per unit, and here two sources exist which lie in opposite directions.

cost per unit = max(actual over the window; price-list tariff) + storage + services
price floor = (cost of goods + cost per unit + advertising) / (1 − required margin)

The tariff knows nothing about oversized parcels and distant warehouses, so it understates the cost. The actual over a lucky window understates it too: the expensive shipments did not fall into it. A floor set too low sells at a loss, a floor set too high merely leaves some orders on the table, so the larger number wins.

The source is visible in the row

Under the number it says either max(actual over the window; tariff) or price-list tariff. The second means the item had no revenue in the window, and that number cannot be leaned on.

Storage and services are added

The tariff does not contain them at all, so they are not an alternative to it. Alternatives are combined with max, additions with a plus: different things are counted differently.

Costs as a share of revenue

Not roubles divided by pieces. Money comes from transactions while the piece counter comes from postings, and they disagree: a day can have revenue with a zero counter. Dividing by such a counter made logistics cost more than the product.

The margin control tab: cost in the cell, cost per unit with its source, margin and the price floor
The cost is edited right in the cell and goes to the cost module, not to the price settings: it has its own cascade, history and author. The change threshold is one-sided: raising it is free, only a drop is guarded.
05 Resolving disputes

Four tools pull the price in different directions

The order in which they apply is pinned down in one place instead of being scattered across the tools. Otherwise two layers start moving the price in circles and there is nothing left to explain the result with.

1. Strategysets the starting point
2. Rivalmay push the price down, but never below the margin floor
3. Out of Stockmay lift it above the rival
4. Medianadds a step on top
5. Pretty pricelimits the size of the jump
6. Minimum pricebeats every add-on, it is the final check

Running out of stock costs more than losing on price. That is why Out of Stock sits after the rival and may override the chase. This case is always written into the disputes: the row gets a "dispute" badge with an explanation instead of a number that changed silently.

The ceiling can move, the floor cannot

A night strategy and a promotion raise lift the ceiling above the base price: that is their whole point. The minimum price is moved by nothing: it is checked last and beats every add-on.

The "decided by" column

Every row shows which tool turned out to be decisive. Without that column a price table becomes an oracle: there is a number, and nobody to ask.

The rivals tab: rival shelf prices and the order in which the tools apply
Rivals are added by hand. Automatic picking from the shelf was dropped after a live check: the shelf offers neighbouring category products, not rivals for this product at this price.
Per-item message feed: what the engine did with the price and why
The per-item message feed: a new recommendation, a dispute between tools, or "hit the floor". Test mode: the system writes nothing back to the marketplace.
06 The shelf price

Two prices, always as a pair

Under every price, current and suggested alike, a second line carries the shelf price after the marketplace discount. A manager compares against rivals by the shelf price, not by the seller price.

As a pair, and here is why. A single shelf price reads as "this is what the shelf shows right now". The client opened a product card, saw one number on the marketplace and could not match it with the one on the screen. There was no error: the number on the screen was the shelf price of the recommendation, while today's seller price gives a different shelf price. Since then the two always stand together.

The engine computes, the screens read

The shelf price is computed by the same pipeline as the recommendation. A conversion of its own on the screen would mean different discounts in different places: the last measurement against the period average.

No measurement means a dash with a reason

If the marketplace discount could not be measured, a dash stands there instead of a price, not the seller price. A substituted default would show a number nobody can compare with rivals.

Measured from actual postings

The marketplace storefront is closed by a bot challenge even with a browser fingerprint. So the discount is computed from the prices of actual postings: the accuracy is about half a percent, and it is stated honestly.

07 What the live account showed

Eight facts that changed the implementation

The spec was written from the marketplace documentation. The live account answered differently, and every such answer became a line in a report with a sample.

What the documentation promisedWhat actually happened
The funnel returns impressions, clicks, cart and conversions.It returns only revenue and ordered units, the rest fails with a deprecated metrics error. Those columns are dashes with the reason, not zeroes.
The brand sits in an attribute with a fixed number.The number depends on the category and is resolved through the attribute reference.
The price method returns the marketplace item id.It does not: only the product id and the seller code. The id is filled in from the catalog, otherwise the table looks populated while the join is empty.
The storefront can be read to measure the discount.The storefront is closed by a bot challenge even from a local address. The discount is computed from actual postings.
The catalog is made of single products.Two thirds of it are sets and multipacks. Cost is tracked per whole item.
Several reports can be active at once.Only one report can be active, so its id is stored and after a restart the report is downloaded rather than ordered again.
Discount limits can be any value.Only fixed values from a list are accepted, the rest are rejected.
A posting contains unique item codes.The same code can appear twice, and the whole batch was rejected. Duplicates now collapse before the write and the quantities are summed.
08 Results

What changed for the client

BeforeAfter
Prices were changed by hand, on the seller price. A recommendation with a pair of prices: the seller price and the shelf price, for the current one and the suggested one alike.
Rules conflicted and moved the price in circles. The order of application is pinned down in one place, disputes are recorded and shown as a badge with an explanation.
The minimum price was set by eye. The floor comes from the required margin on the conservative max(actual; tariff) principle, with the source visible.
Cost appeared in the table twice. One cell, and the same cell is editable, and the edit goes to the cost module with its own history and author.
Rivals were picked automatically. The list is filled in by hand: automatic picking returned neighbouring category products, not rivals. Records are muted rather than deleted, so a rival's price history survives.
It was unclear why the price was what it was. A "decided by" column and a per-item message feed: what the engine did with the price and why.

Test mode by the client's decision. The system writes nothing back to the marketplace: the result is a recommendation on the product card, an alert and a message in the feed. There is not a single write method in the marketplace clients. That is how trust in the engine is earned before it is given the wheel.

Kirill Sedov

Business analyst and systems builder. I start with the business process and the data contract, then automate. Here the pricing decision stopped being a guess and learned to explain itself in one line.

Get in touch on LinkedIn