Inspiration
Algorithmic pricing was supposed to make markets more competitive. Instead, research shows that independent learning agents can settle on high, stable prices without ever sending a message to each other. The DOJ’s case against RealPage brought that idea out of the lab and into rent and grocery bills. We wanted a tool that could show the mechanism clearly in simulation, then ask the same statistical questions on real store prices, without pretending a dashboard can convict anyone.
CartelBreaker started as a question: if two bots can learn tacit collusion from first principles, how would a regulator or researcher recognize the pattern in the wild?
What we learned
Collusion in learning systems is not just “prices look high.” Klein (2021) emphasizes reward and punishment: one agent undercuts, the other retaliates, and both learn that cooperation pays. That is why our simulator includes a poke audit on agents we control, while the real-market channel stays honest and only screens for rigidity and lockstep movement.
On real data, a single snapshot is rarely enough. Abrantes-Metz et al. (2006) motivate a variance screen: coordinated pricing often shows abnormally low dispersion relative to level. We combined that intuition with lockstep co-movement between stores over time. We also learned that web data is messy: live shopping search gives fresh cross-retailer prices, but historical panels need a fallback. That is why we built a Nimble-first pipeline with Open Prices as the depth layer, and why the UI always says which source served the current screen.
How we built it
The project has two halves that share one statistical core.
Simulated lab. We reproduce a logit-Bertrand market with Q-learning agents (Calvano et al., 2020). The detector scores supracompetitive pricing with a delta gauge. The audit forces a deviation and watches for punishment. The disruptor injects heterogeneity to break the cartel. The detector and engine stay agent-agnostic: they never import agent classes, so the same machinery can later plug in LLM pricing bots.
Real-market screen. The Streamlit app is styled as a vintage TV: turn the cabinet knob to switch channels. Channel 2 searches a grocery product, filters by city and store, plots prices with Plotly, and returns a risk tier (low, watch, elevated). Data flows through fetch_market_data: Nimble Shopping search runs first when a server-side key is configured; Open Food Facts Open Prices supplies historical observations when Nimble is thin or unavailable. Observations are normalized into one schema before screen_food_prices runs.
The screen uses two familiar signatures. Rigidity is low coefficient of variation across prices:
$$\mathrm{CV} = \frac{\sigma}{\mu}$$
Lockstep is high correlation between competitors’ price series over time. Together they surface candidates; they do not prove collusion.
We backed the story with tests: positive and negative controls, a confusion matrix on labeled regimes, and checks that a genuine cartel can flip from red to green when we break it in simulation.
Challenges
The hardest part was keeping the narrative honest. On real sellers we cannot force an undercut, so we refuse to output a “verdict.” Every real-market result is a flag, with copy that points to regulator subpoenas as the real standard of proof.
Data engineering was the second challenge. Retail sites block naive scrapers, search snippets omit prices, and grocery products have variants and bundles. We layered Nimble web unlock, direct HTTP backfill, and Open Prices history, then surfaced the active layer in the UI so demos stay truthful.
The third challenge was presentation. Antitrust economics can feel abstract, so we wrapped the method in a CRT “TV guide” story channel, interactive charts, and an illustrative overlay that shows what coordinated pricing would look like in the same market without passing it off as observed fact.
Built with
See the “Built with” field below; the stack is Python-first with Streamlit on the front end and Nimble plus Open Prices on the data side.
Log in or sign up for Devpost to join the conversation.