DEV Community

Anakin
Anakin

Posted on

Dynamic Pricing Rules That Do Not Accidentally Burn Your Margins

Dynamic pricing usually fails in boring ways. A competitor price is stale, inventory sync lags by ten minutes, or a rule lowers a product below contribution margin because nobody encoded the actual floor. The hard part is not changing prices. The hard part is deciding when a price change is safe.

Treat pricing as a decision system, not a cron job

A naive implementation often starts like this:

if (competitorPrice < myPrice) {
  myPrice = competitorPrice - 1;
}
Enter fullscreen mode Exit fullscreen mode

That works until the competitor runs a loss-leading flash sale, their page parser reads the wrong variant, or your own inventory is almost gone and you should be raising price instead of chasing them down.

A better model separates inputs, rules, and guardrails:

  • Inputs: current price, cost, stock, recent sales, competitor prices, timestamps
  • Rules: when to increase or decrease price
  • Guardrails: absolute constraints that no rule can bypass
  • Observability: why the system chose the new price

For example, a gaming console might use demand-based pricing with competitor awareness:

  • Increase price by 12% if more than 50 units sold in 24 hours
  • Decrease price by 7% if fewer than 15 units sold in 24 hours
  • Do not go below $329
  • Do not go above $479
  • Ignore competitor prices older than 30 minutes
  • Do not change price more than once every 2 hours

Those last two rules matter more than they look. Without them, prices can oscillate all day or react to bad data.

Model the data you need before writing the rule

You need more than sku and price. Store enough context to explain the decision later.

create table price_observations (
  id bigserial primary key,
  sku text not null,
  source text not null,
  observed_price numeric(10,2) not null,
  observed_at timestamptz not null,
  metadata jsonb not null default '{}'
);

create table pricing_decisions (
  id bigserial primary key,
  sku text not null,
  old_price numeric(10,2) not null,
  new_price numeric(10,2) not null,
  reason text not null,
  inputs jsonb not null,
  decided_at timestamptz not null default now()
);
Enter fullscreen mode Exit fullscreen mode

The price_observations table lets you distinguish between “Amazon is cheaper” and “our scraper last saw Amazon cheaper 4 hours ago.” Those are different facts.

Competitor extraction is also where many pricing systems get noisy: variant pages change, shipping gets included inconsistently, and blocked requests can look like missing products if you do not model failure separately. Wire is useful in this part of the pipeline when competitor prices need to be collected as explicit observations with inspectable failures instead of treated as always-fresh inputs.

Encode guardrails as code, not documentation

Here is a small TypeScript example. It is intentionally plain: no ML, no personalization, no magic. Just deterministic pricing with traceable reasons.

type PricingInput = {
  sku: string;
  currentPrice: number;
  unitCost: number;
  stock: number;
  unitsSold24h: number;
  competitorPrice?: number;
  competitorObservedAt?: Date;
  lastPriceChangeAt?: Date;
  now: Date;
};

type PricingDecision = {
  newPrice: number;
  reason: string;
  blocked?: string;
};

const MIN_MARGIN = 0.12;
const PRICE_FLOOR = 329;
const PRICE_CEILING = 479;
const MAX_COMPETITOR_AGE_MINUTES = 30;
const MIN_CHANGE_INTERVAL_MINUTES = 120;

function minutesBetween(a: Date, b: Date) {
  return Math.abs(a.getTime() - b.getTime()) / 60_000;
}

function roundPrice(price: number) {
  return Math.round(price * 100) / 100;
}

function clamp(price: number, min: number, max: number) {
  return Math.min(Math.max(price, min), max);
}

export function decidePrice(input: PricingInput): PricingDecision {
  if (
    input.lastPriceChangeAt &&
    minutesBetween(input.now, input.lastPriceChangeAt) < MIN_CHANGE_INTERVAL_MINUTES
  ) {
    return {
      newPrice: input.currentPrice,
      reason: "no_change",
      blocked: "price_changed_too_recently"
    };
  }

  const marginFloor = input.unitCost * (1 + MIN_MARGIN);
  const effectiveFloor = Math.max(PRICE_FLOOR, marginFloor);

  let candidate = input.currentPrice;
  let reason = "no_change";

  if (input.unitsSold24h >= 50 && input.stock > 20) {
    candidate = input.currentPrice * 1.12;
    reason = "high_demand_24h";
  } else if (input.unitsSold24h < 15 && input.stock > 50) {
    candidate = input.currentPrice * 0.93;
    reason = "low_demand_high_stock";
  }

  const competitorIsFresh =
    input.competitorPrice !== undefined &&
    input.competitorObservedAt !== undefined &&
    minutesBetween(input.now, input.competitorObservedAt) <= MAX_COMPETITOR_AGE_MINUTES;

  if (competitorIsFresh && input.competitorPrice! < candidate) {
    candidate = input.competitorPrice! - 1;
    reason = "fresh_competitor_undercut";
  }

  const guarded = clamp(candidate, effectiveFloor, PRICE_CEILING);

  return {
    newPrice: roundPrice(guarded),
    reason
  };
}
Enter fullscreen mode Exit fullscreen mode

This code makes a few tradeoffs visible.

It refuses to change prices too often. That reduces reactivity, but it also prevents price bouncing when competitors and your own system keep reacting to each other.

It treats stale competitor data as unusable. That means you may miss a short sale, but you avoid changing prices based on a page snapshot that no longer reflects the market.

It calculates a margin-based floor in addition to a business-defined floor. If cost changes and nobody updates the static floor, the system still avoids selling below the configured minimum margin.

Test the ugly cases

Do not only test the expected demand spike. Test the cases that would create support tickets.

import { expect, test } from "vitest";
import { decidePrice } from "./pricing";

test("does not use stale competitor price", () => {
  const now = new Date("2026-01-10T12:00:00Z");

  const decision = decidePrice({
    sku: "console-1",
    currentPrice: 399,
    unitCost: 280,
    stock: 80,
    unitsSold24h: 10,
    competitorPrice: 299,
    competitorObservedAt: new Date("2026-01-10T10:00:00Z"),
    now
  });

  expect(decision.newPrice).toBe(371.07);
  expect(decision.reason).toBe("low_demand_high_stock");
});
Enter fullscreen mode Exit fullscreen mode

Other tests worth adding:

  • Competitor price would push below cost plus margin
  • Current stock is low, so low-price rules should not fire
  • Price ceiling blocks a demand spike increase
  • Last price change happened 20 minutes ago
  • Competitor price is for the wrong variant or missing shipping

That last one is not purely a unit test problem. You need validation before observations enter the pricing engine. If the competitor page contains multiple variants, store the matched variant identifier and confidence score, or reject the observation.

Roll out with a kill switch

Start with one category and a small SKU set. Electronics, accessories, and seasonal products usually show pricing effects faster than low-volume premium products.

Track at least these metrics:

  • Conversion rate
  • Gross margin per order
  • Units sold
  • Cart abandonment
  • Price changes per SKU per day
  • Percentage of decisions blocked by guardrails

The last metric tells you whether your rules are fighting your constraints. If 60% of proposed changes hit the floor, your discount logic probably ignores cost. If many decisions are blocked because data is stale, fix ingestion before tuning pricing.

Also add a kill switch:

if (process.env.DYNAMIC_PRICING_ENABLED !== "true") {
  return { newPrice: input.currentPrice, reason: "disabled" };
}
Enter fullscreen mode Exit fullscreen mode

Dynamic pricing is operational software. It touches revenue, customer trust, and sometimes marketplace compliance. Build it like something that can fail, because it will.

A good next step is to pick five SKUs, write down the exact inputs and guardrails for each, then run your pricing function in shadow mode for a week before allowing it to update the catalog.

Top comments (0)