DEV Community

Anakin
Anakin

Posted on

Automated Repricing Without Turning It Into a Race to the Bottom

Automated repricing usually fails in a very specific way: someone turns on “match the lowest competitor” and the system does exactly that. A competitor clears stock at a loss, your prices follow, and nobody notices until margin reports look wrong a month later.

The hard part is not changing prices automatically. The hard part is deciding when the system is allowed to change a price, when it must hold, and when it should ask a human.

Treat repricing as rules, not reactions

A useful repricer should not answer “what is the lowest market price?” and stop there. It needs enough context to answer a better question:

Given our costs, margin target, stock position, and relevant competitors, what price are we allowed to set?

That distinction matters. If your pricing logic treats every competitor equally, you will react to marketplace noise: liquidators, sellers with bad availability, suspicious listings, or out-of-stock products that still expose stale prices.

At minimum, model these inputs:

  • Your current price
  • Product cost
  • Fulfillment cost
  • Minimum margin
  • Maximum allowed price
  • Competitor prices
  • Competitor stock status
  • Competitor priority
  • Minimum change threshold

The threshold prevents churn. If a competitor moves from $99.99 to $99.49, you probably do not need to reprice. If they undercut you by 8% on a high-volume SKU and you still have margin room, you might.

A small repricing function

Here is a simplified JavaScript implementation. It is not a full pricing system, but it shows the shape of the logic.

function calculateFloorPrice({ productCost, fulfillmentCost, overhead, minMargin }) {
  const allInCost = productCost + fulfillmentCost + overhead;
  return roundMoney(allInCost * (1 + minMargin));
}

function roundMoney(value) {
  return Math.round(value * 100) / 100;
}

function choosePrice({ sku, currentPrice, costs, ceilingPrice, competitors, rules }) {
  const floorPrice = calculateFloorPrice(costs);

  const relevantCompetitors = competitors
    .filter(c => rules.trackedCompetitors.includes(c.name))
    .filter(c => c.inStock === true)
    .filter(c => c.price > 0);

  if (relevantCompetitors.length === 0) {
    return {
      sku,
      action: "hold",
      price: currentPrice,
      reason: "no relevant in-stock competitor prices"
    };
  }

  const lowestRelevant = relevantCompetitors
    .sort((a, b) => a.price - b.price)[0];

  const priceGap = (currentPrice - lowestRelevant.price) / currentPrice;

  if (priceGap < rules.minGapToReact) {
    return {
      sku,
      action: "hold",
      price: currentPrice,
      reason: "competitor gap below threshold"
    };
  }

  const targetPrice = roundMoney(lowestRelevant.price - rules.undercutBy);

  if (targetPrice < floorPrice) {
    return {
      sku,
      action: "review",
      price: currentPrice,
      reason: `target price ${targetPrice} is below floor ${floorPrice}`
    };
  }

  return {
    sku,
    action: "update",
    price: Math.min(targetPrice, ceilingPrice),
    reason: `reacting to ${lowestRelevant.name}`
  };
}

const result = choosePrice({
  sku: "HEADPHONES-123",
  currentPrice: 129.99,
  costs: {
    productCost: 82,
    fulfillmentCost: 7.5,
    overhead: 4,
    minMargin: 0.18
  },
  ceilingPrice: 149.99,
  competitors: [
    { name: "RetailerA", price: 119.99, inStock: true },
    { name: "RetailerB", price: 89.99, inStock: false },
    { name: "RandomMarketplaceSeller", price: 79.99, inStock: true }
  ],
  rules: {
    trackedCompetitors: ["RetailerA", "RetailerB"],
    minGapToReact: 0.05,
    undercutBy: 1.00
  }
});

console.log(result);
Enter fullscreen mode Exit fullscreen mode

This returns an update based on RetailerA, not the marketplace seller, and not the out-of-stock RetailerB price.

The important part is the failure mode. If the calculated target price falls below the floor, the function returns review instead of forcing a bad price into production. That gives your pricing team a queue of exceptions instead of a hidden margin problem.

Competitor data can break the whole system

A repricer is only as good as the data feeding it. Bad competitor data usually fails quietly:

  • A page scrape reads a promotional badge as the actual price
  • A stale product page reports an old discount
  • An out-of-stock listing still has a low visible price
  • A marketplace variant maps to the wrong SKU
  • A currency or tax difference gets ignored

Those are not edge cases in retail. They happen constantly.

If you run repricing from a daily CSV, you also have latency built into the process. By the time the file lands, gets processed, and updates prices, the competitor state may already have changed. That is fine for slow categories like furniture. It is risky for electronics, grocery, or apparel basics.

Wire fits this part of the stack when your repricer needs fresh competitor price extraction with stock state and timestamps instead of manual checks or stale CSV uploads.

Guardrails should be explicit and testable

Do not bury margin protection in spreadsheet notes or admin settings nobody reviews. Put the rules somewhere you can test.

For example, this case should never update the price:

const belowFloor = choosePrice({
  sku: "SKU-1",
  currentPrice: 100,
  costs: {
    productCost: 80,
    fulfillmentCost: 8,
    overhead: 2,
    minMargin: 0.2
  },
  ceilingPrice: 130,
  competitors: [
    { name: "RetailerA", price: 89, inStock: true }
  ],
  rules: {
    trackedCompetitors: ["RetailerA"],
    minGapToReact: 0.05,
    undercutBy: 1
  }
});

if (belowFloor.action !== "review") {
  throw new Error("Expected repricer to block price below floor");
}
Enter fullscreen mode Exit fullscreen mode

This is the kind of test that catches expensive mistakes. Without it, a supplier cost increase can turn yesterday’s acceptable floor into today’s loss-making price.

Ceiling prices matter too. If all competitors go out of stock, you do not want an algorithm raising a $40 item to $400 because it sees no market anchor. Use MSRP, a recent historical high, or a median market benchmark as the cap.

Start with a small category

Do not automate a full catalog first. Pick 100 to 200 SKUs in a category where you already understand the margins and competitive set.

For the first month, review every automated decision:

  • How often did the system hit the floor?
  • Which competitors caused most price changes?
  • How many updates became manual reviews?
  • Did sales velocity improve?
  • Did gross margin stay inside the expected range?

After that, reduce review frequency only if the exceptions make sense. The goal is not to remove humans from pricing strategy. It is to remove the repetitive work of checking prices and making obvious changes.

A practical next step: take one SKU file, add all-in cost, floor price, ceiling price, competitor priority, and stock-aware competitor prices, then run your pricing rules in dry-run mode for two weeks before writing any price back to production.

Top comments (0)