Part of the Trust Engineering series, where we open-source the math behind GoBuy's product trust scores. Code is Python; the ideas are deliberately simple enough to reimplement in an afternoon.

A few weeks ago someone asked us a question that sounds trivial and isn't: "How do you tell the difference between a product going viral and a product being review-bombed with fakes?"

Both look identical at first glance. A product that sat at 40 reviews for eight months suddenly gets 300 five-star reviews in two days. Maybe a TikTok went viral. Maybe a seller bought 300 reviews. The aggregate rating — the only thing most tools look at — moves the same direction in both cases.

The difference is in the shape of arrival times. This post walks through how we detect review bursts, why we score inflation and suppression bursts differently, and the two statistical traps we fell into before landing on the current design.

Why aggregate ratings can't see this

If you average 2,000 reviews, adding 300 more barely moves the mean. That's the whole point of a mean — it's robust to dilution. Coordinated manipulators know this: they don't need to flip your average, they only need to nudge it a few points and keep it there. Aggregates are also memoryless about ordering: a burst at the start of a product's life and a burst last Tuesday produce the same star average.

So the first design decision: treat review arrival as a time series, not a bag of stars.

Step 1: Model arrivals as a Poisson process

Organic review traffic for a mature product is roughly stationary: reviews trickle in at a rate λ proportional to current sales volume. That makes arrival counts in fixed windows approximately Poisson.

For each product we maintain an EWMA of the arrival rate over the trailing 90 days, then test each day's observed count k against Poisson(λ̂):

from scipy import stats def burst_score(daily_counts, ewma_lambda, window=3): """Probability that the last `window` days are NOT explained by the product's own organic arrival rate.""" expected = ewma_lambda * window observed = sum(daily_counts[-window:]) # upper tail: how surprising is this burst, given normal traffic?  p = stats.poisson.sf(observed - 1, expected) surprise = 1.0 - p return surprise 

A surprise above 0.999 on a mature product means the last three days saw traffic that the product's own history essentially cannot explain. By itself, that's a viral product OR a manipulation campaign. It's a tripwire, not a verdict.

Step 2: Condition the burst on what kind of reviews arrived

This is the step that separates virality from manipulation, and it comes straight from the data: organic bursts are rating-diverse, manufactured bursts are not.

When real humans discover a product, they arrive with real experiences — you get a spread of 4s and 5s, some 3s, the occasional angry 1-star. When a seller buys reviews, they buy one SKU: 5-star, unverified, short text. So we split the burst by rating value and verification status and compute the surprise score per segment:

def manipulation_evidence(burst_reviews, product_baseline): five_star = [r for r in burst_reviews if r.rating == 5] unverified = [r for r in five_star if not r.verified_purchase] # share of burst that is 5-star vs product's historical 5-star share  five_share = len(five_star) / len(burst_reviews) share_uplift = five_share - product_baseline.five_star_share # unverified ratio among the 5-star burst  unverified_ratio = len(unverified) / max(len(five_star), 1) return share_uplift, unverified_ratio 

Our operating heuristic after tuning on labeled data:

  • Burst + 5-star share ≥ 0.9 + unverified ratio ≥ 0.6 → inflation campaign. Strong evidence.
  • Burst + ratings spread matches historical → probably genuine virality. No penalty, just flag for re-scoring.
  • Burst of 1-stars → review bombing (a competitor, a brigade). Also penalized, but scored separately, because the product may be fine — the review section is what's poisoned.

That last case matters more than people realize. Roughly a third of the bursts we see are negative, and naive detectors that only hunt 5-star spikes misclassify them as "engagement."

Step 3: The trajectory view — bursts in context

A burst in isolation is ambiguous; a burst in a trajectory usually isn't. We bucket every product's rating history into three regimes:

  1. Steady state — arrival rate flat, rating distribution stable.
  2. Step change — arrival rate shifts to a new plateau (price cut, ranking jump, viral moment).
  3. Spike and decay — a burst that fades back to baseline.

The dangerous signature is spike-and-decay with a rating gap: the burst reviews average 4.9 while the pre-burst baseline averages 3.8 and the post-burst reviews snap back to 3.9. Real virality brings in customers whose experiences match the baseline population. Manufactured bursts insert a population whose experiences were never real, so the ratings before and after agree with each other and disagree with the burst. When before/after agree and the burst is the outlier, the burst is the anomaly — regardless of content.

def rating_gap(baseline_before, burst, baseline_after): return abs(burst.mean - (baseline_before.mean + baseline_after.mean) / 2) 

A gap above ~0.8 stars with burst volume > 10% of lifetime reviews is, in our labeled set, almost always manipulation.

The two traps

Trap one: young products. Everything above assumes an EWMA baseline exists. A product three days old with 200 reviews has no baseline; the burst detector is undefined. For young products we fall back to category priors (how fast do products in this category normally accumulate reviews) — that's a whole separate post.

Trap two: legitimate sales events. Prime Day produces genuine bursts that look structurally like campaigns: volume spike, 5-star heavy, verified though — which is the tell. Verification status is the single highest-signal feature we have, which is also exactly why it's the one the paid-review market has been hardest to fake at scale.

What the agent sees

All of this condenses to one flag and one number in our MCP output:

{ "smart_score": 31, "flags": ["temporal_burst", "rating_gap"], "burst": {"window_days": 3, "surprise": 0.9998, "direction": "inflation"} } 

An AI shopping agent doesn't need to reimplement any of this — it needs a reason not to recommend the product, expressed in one line. The full scoring spec is documented at docs.gobuy.ai/standard.

Takeaways if you're building anything similar: model arrivals, not aggregates; condition bursts on rating composition and verification; and always check whether the burst is the outlier relative to its own before/after baselines. The math is 60 lines of Python. The labeled data is the hard part — and the reason we publish the method instead of the model.

Questions, edge cases, or products you think would break this? That's genuinely what the comments are for.