DEV Community

Cover image for A Pricing Model's Output Isn't a Price. It's a Suggestion With No Leash On It.
Snehasish Konger
Snehasish Konger

Posted on

A Pricing Model's Output Isn't a Price. It's a Suggestion With No Leash On It.

We ran into this exact problem building a dynamic pricing engine on Nected for a marketplace client last year. Their data science team had already shipped a demand model — solid work, genuinely good at predicting elasticity. The problem showed up somewhere nobody had assigned an owner to: the gap between what the model returned and what actually got charged to a customer. Nobody had decided, in any deliberate way, what happens when the model's confidence and the business's tolerance for risk disagree.

That gap is what this post is actually about. We ended up building it as a rules layer on Nected, sitting directly between the model and checkout, and most of what we learned came from getting that layer wrong twice before getting it right.

A demand model doesn't know what a price is. It knows what a number correlated with historical outcomes is. Feed it the right features and it hands back a multiplier, a probability, an elasticity-adjusted score — something continuous, something that looks like a decision but isn't one yet. Ship that number straight to checkout as "the price" and you've skipped the actual engineering problem. The hard part is the layer in between: the deterministic caps, floors, and bounds that turn a probabilistic score into a number you're willing to charge a real customer, at 2am, with no engineer watching.

What the model is actually good at, and bad at

The client's model was good at exactly one thing: finding correlations between demand signals and historical price sensitivity across a large feature space — time of day, inventory levels, competitor pricing, weather, local events — faster and more consistently than any person could. Feed it enough historical data and it outputs a multiplier that, on average, captures more value than a static price would.

What it wasn't good at, and what nearly caused an incident during our first pilot week, was knowing when it had extrapolated into territory the training data never covered. A regional event none of the historical data accounted for pushed inputs outside the distribution the model had learned from, and the model didn't know to flag uncertainty — it just returned a multiplier with the same confidence it had on an ordinary Tuesday. In this case that number came out well above what any customer would tolerate, mathematically consistent with the training signal and completely indefensible as an actual price. That was the moment the client's team agreed the rule layer wasn't optional polish — it was the thing standing between the model and a very bad headline.

Why raw model output can't go straight to the customer

This isn't a one-off failure mode specific to that client. Research on dynamic pricing in on-demand mobility markets has documented anomalous supply shortages that emerge specifically from unconstrained dynamic pricing feedback loops — cases where the pricing mechanism itself, responding to its own outputs in a tight loop, produces the exact shortage and price spike it was supposed to prevent. The model isn't malfunctioning in these cases. It's doing exactly what it was trained to do, in a situation where "exactly what it was trained to do" is the wrong answer.

That's the actual argument for a guardrail layer, and it's the argument we made internally to justify the extra build time. It's not "the model might have a bug." It's "the model's job is to be a good statistical predictor, and being a good statistical predictor is a different job than being a safe, explainable decision — the two only look the same in the range of inputs the model has already seen."

The guardrail layer we built

The pattern we landed on is deliberately boring: the model's output is one input among several to a deterministic function that clamps, bounds, and can override it entirely. Here's roughly what the logic looked like before we moved it into Nected's rule builder:

def apply_guardrails(model_multiplier: float, context: PricingContext) -> PriceDecision:
    base_price = context.base_price
    raw_price = base_price * model_multiplier

    # Hard floor and ceiling — never negotiable, regardless of model confidence
    floor = base_price * 0.85
    ceiling = base_price * context.category_max_multiplier  # e.g. 2.5x for rides, 1.3x for groceries

    clamped = max(floor, min(raw_price, ceiling))

    # Rate-of-change cap: price can't jump more than 15% from its last published value
    max_delta = context.last_price * 0.15
    if abs(clamped - context.last_price) > max_delta:
        clamped = context.last_price + max_delta * (1 if clamped > context.last_price else -1)

    # Regulatory or contractual override beats everything above it
    if context.regulatory_cap is not None:
        clamped = min(clamped, context.regulatory_cap)

    return PriceDecision(
        price=round(clamped, 2),
        model_multiplier=model_multiplier,
        was_clamped=clamped != round(raw_price, 2),
    )
Enter fullscreen mode Exit fullscreen mode

Three separate constraints, stacked deliberately: an absolute floor and ceiling per category, a rate-of-change cap so a price can't jump violently between two consecutive quotes even if it stays within the absolute bounds, and a regulatory override that beats both because some price ceilings — insurance, lending, certain consumer categories — aren't a business decision at all. Each one is independently auditable: you can point at was_clamped and know exactly when the model's raw suggestion and the actual charged price diverged, and why. That auditability turned out to matter more to the client's finance team than the pricing logic itself.

Where this fails in practice

The first version we shipped hardcoded these caps as constants in the same service that called the model, which meant changing category_max_multiplier for a new product line needed the same deploy cycle as retraining the model. That's the failure mode we ran into directly: the guardrail logic ages badly for the same reason inline business rules always do. Nobody planned to make it configurable because it looked like a constant at ship time, and three months later it was a pricing policy decision trapped in application code, owned by whichever engineer happened to write it.

There's a real engineering case for treating this as its own layer rather than inline logic, and it's not just our experience — recent work on hardening ML systems for production frames it explicitly: wrapping a model's output in a dedicated rules layer that validates and bounds predictions before they reach a downstream system is a first-class software engineering pattern, not an afterthought, the same way input validation isn't optional just because the rest of the request looks fine.

How we solved it on Nected

This is the part we rebuilt once we understood the actual requirement, and it's the version that's been running in production since. The floors, ceilings, rate-of-change caps, and regulatory overrides all moved into Nected's visual rule builder, which meant the client's pricing and revenue team — not engineering — could own category_max_multiplier and the rest of the guardrail constants directly. Every change to those caps is versioned with a full audit trail, so "why did this price get clamped on this date" has an actual answer instead of a git blame on a constants file, which is exactly the question their finance team asked us in week one.

The split that made this click for their engineering team: a rule engine vs machine learning comparison isn't about which one is smarter — it's that a model is built to find patterns in ambiguous data, and a rule engine is built to enforce a decision deterministically once the ambiguity has to end. The dynamic pricing rule engine we ended up with on Nected is that second half, on purpose, running as a stateless call the pricing service hits after the model returns its multiplier. If you're earlier in this process than we were, it's worth reading through what problems a dynamic pricing engine actually solves before assuming your first version of the guardrail constants will scale past the one product line you wrote them for — ours didn't, and that's the rebuild described above.

When you don't need this

Worth saying plainly, because we'd have saved ourselves some early effort if someone had told us this up front: if you're running a small catalog with prices that change quarterly based on a spreadsheet someone updates by hand, none of this applies. No model in the loop means no guardrail problem — you have a pricing policy problem, which is simpler to solve and doesn't need a rules platform at all. This architecture earns its cost specifically when a model's output reaches a customer without a human checking it first. Short of that, a floor and ceiling in a config file, reviewed the old-fashioned way, is genuinely adequate.

FAQ

Why can't a pricing model just set the final price directly?
Because a model optimizes for statistical accuracy against historical data, not for what's safe or defensible to charge a real customer in a moment the training data never covered. It has no built-in concept of "this number is too extreme to ship" — that judgment has to live in a separate, deterministic layer.

What's the difference between a guardrail layer and just clamping the output in code?
Nothing, structurally — a guardrail layer is that clamping logic. The difference is where it lives. Buried inline in the pricing service, it's a hidden constant only engineering can change. Moved into a dedicated rule layer, it's a governed, versioned, business-owned policy that can change without a deploy.

Does a guardrail layer slow down the pricing response?
Not meaningfully if it's built as a stateless, deterministic function — no external calls, no waiting. In our case, the added latency from the rule evaluation was negligible next to the model inference time it followed.

Who should own the guardrail constants — engineering or the business?
The business, in most cases. Floors, ceilings, and rate-of-change caps are pricing policy decisions, not engineering decisions. The engineering job is building the layer that lets pricing and revenue teams change those numbers safely, with an audit trail, without needing a deploy.

What happens if the guardrail layer and the model disagree?
The guardrail always wins. That's the entire point of the layer — it's the deterministic backstop, not a second vote. The model's raw output gets logged for analysis regardless, so you can see how often clamping actually happens and whether the caps need revisiting.

Top comments (0)