Skip to content
Dylan San GabrielDylan
Get in touch

GET /api/work/internal-erp/repricing

Repricing every minute without a race to the bottom

Live · · sole engineer

Prices used to be a spreadsheet someone updated every morning. Now a robot checks every product once a minute, and knows better than to start a price war.Replaced a morning pricing spreadsheet with an engine that reprices every minute, with cooldowns, floors, and a rule to never chase the lowest price.

  • TypeScript
  • Node.js
  • PostgreSQL
  • Python
  • Flask
  • Web scraping
  • Net32 API
price pushes logged
138K+
sweep interval
60s
cooldown per product
12h
pricing engines
1
The repricer dashboard: buy-box winning, sibling-hold, losing and unwinnable counts, push success rates, a breakdown of why the engine isn't undercutting, per-storefront buy-box share, and the safety rails including a 15% margin floor. A second storefront's name is blurred.
The repricer dashboard: buy-box winning, sibling-hold, losing and unwinnable counts, push success rates, a breakdown of why the engine isn't undercutting, per-storefront buy-box share, and the safety rails including a 15% margin floor. A second storefront's name is blurred.
GET/api/work/{id}

Parameters

Request

curl "https://portfolio.dylansg0318.workers.dev/api/work/internal-erp/repricing"
source: src/content/projects/{id}

Response200 OK · 3.3 KB

{
  "id": "internal-erp/repricing",
  "title": "Repricing every minute without a race to the bottom",
  "status": "live",
  "year": 2026,
  "category": "work",
  "role": "sole engineer",
  "summary": "Replaced a morning pricing spreadsheet with an engine that reprices every minute, with cooldowns, floors, and a rule to never chase the lowest price.",
  "summary_plain": "Prices used to be a spreadsheet someone updated every morning. Now a robot checks every product once a minute, and knows better than to start a price war.",
  "stack": [
    "TypeScript",
    "Node.js",
    "PostgreSQL",
    "Python",
    "Flask",
    "Web scraping",
    "Net32 API"
  ],
  "metrics": {
    "price_pushes_logged": "138K+",
    "sweep_interval": "60s",
    "cooldown_per_product": "12h",
    "pricing_engines": "1"
  },
  "parent": {
    "id": "internal-erp",
    "title": "The internal ERP that runs a dental supply company",
    "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp"
  },
  "problem": "Prices on Net32 were set from a spreadsheet someone updated every morning by looking at competitors. By noon it was stale. By the next morning we'd either lost the buy box or given away margin overnight. The first automation, a Python scraper, helped, but it created a second problem: two pricing paths that could disagree, so the \"optimal price\" the dashboard showed wasn't always the price the job pushed.",
  "unique": "Two decisions carry the whole thing. First, never chase the lowest price: the tiered strategy targets the buy-box winner in the chosen mode, holds when undercutting would breach the floor, and pushes only when a competitor has actually moved and the 12-hour cooldown has cleared. Second, one engine everywhere, the manual button, the bulk push, the auto mode, the post-scrape recompute and the \"next run\" preview all call the same function, so the preview can never lie about what the job will do.",
  "ai": "The first version was hand-written Python and XPath. The rewrite into the ERP was agent-assisted, but the strategy, what counts as a competitor, when to hold, why one engine, was worked out on paper with the owner first. The agent's most useful contribution was the diagnostic that proves the preview and the push agree.",
  "learned": [
    "Two engines that \"should\" agree will diverge, and you find out from a customer. The fix isn't syncing them. It's deleting one.",
    "A cooldown is a feature, not a limitation. Repricing on every scrape turns two sellers into a price war with a one-minute tick. A per-product anchor with a 12-hour hold made prices stable enough for the buyer to trust.",
    "Log every attempt, including the ones that decided not to push. When margin drops, the question is always \"why didn't it move?\", and \"it ran and chose to hold\" is an answer only if you wrote it down."
  ],
  "cover": {
    "url": "https://portfolio.dylansg0318.workers.dev/_astro/repricer-dashboard.BbPNiDYu.png",
    "width": 1920,
    "height": 1114,
    "alt": "The repricer dashboard: buy-box winning, sibling-hold, losing and unwinnable counts, push success rates, a breakdown of why the engine isn't undercutting, per-storefront buy-box share, and the safety rails including a 15% margin floor. A second storefront's name is blurred."
  },
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/repricing"
}

Live: this is the answer from this site’s API, not a mock. Send re-runs the request with your parameters; if the answer names a page, it opens.

The problem

Prices on Net32 were set from a spreadsheet someone updated every morning by looking at competitors. By noon it was stale. By the next morning we'd either lost the buy box or given away margin overnight. The first automation, a Python scraper, helped, but it created a second problem: two pricing paths that could disagree, so the "optimal price" the dashboard showed wasn't always the price the job pushed.

What was unique

Two decisions carry the whole thing. First, never chase the lowest price: the tiered strategy targets the buy-box winner in the chosen mode, holds when undercutting would breach the floor, and pushes only when a competitor has actually moved and the 12-hour cooldown has cleared. Second, one engine everywhere, the manual button, the bulk push, the auto mode, the post-scrape recompute and the "next run" preview all call the same function, so the preview can never lie about what the job will do.

Where AI fit in

The first version was hand-written Python and XPath. The rewrite into the ERP was agent-assisted, but the strategy, what counts as a competitor, when to hold, why one engine, was worked out on paper with the owner first. The agent's most useful contribution was the diagnostic that proves the preview and the push agree.

GET/api/work/{id}

Parameters

Request

curl "https://portfolio.dylansg0318.workers.dev/api/work/internal-erp/repricing?fields=problem%2Cunique%2Cai"
source: src/content/projects/{id}

Response200 OK · 1.3 KB

{
  "problem": "Prices on Net32 were set from a spreadsheet someone updated every morning by looking at competitors. By noon it was stale. By the next morning we'd either lost the buy box or given away margin overnight. The first automation, a Python scraper, helped, but it created a second problem: two pricing paths that could disagree, so the \"optimal price\" the dashboard showed wasn't always the price the job pushed.",
  "unique": "Two decisions carry the whole thing. First, never chase the lowest price: the tiered strategy targets the buy-box winner in the chosen mode, holds when undercutting would breach the floor, and pushes only when a competitor has actually moved and the 12-hour cooldown has cleared. Second, one engine everywhere, the manual button, the bulk push, the auto mode, the post-scrape recompute and the \"next run\" preview all call the same function, so the preview can never lie about what the job will do.",
  "ai": "The first version was hand-written Python and XPath. The rewrite into the ERP was agent-assisted, but the strategy, what counts as a competitor, when to hold, why one engine, was worked out on paper with the owner first. The agent's most useful contribution was the diagnostic that proves the preview and the push agree.",
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/repricing"
}

Live: this is the answer from this site’s API, not a mock. Send re-runs the request with your parameters; if the answer names a page, it opens.

The story

On a marketplace like Net32 the seller with the right price wins the sale, and competitors change theirs all day. Ours were set from a spreadsheet someone updated every morning by eyeballing the competition. By lunch it was stale. By the next morning we’d either lost sales overnight or quietly given margin away.

So I built the robot version of that morning routine. Every minute it looks at the products due for a check, sees what competitors are charging right now, and decides: move or hold. It never drops below the floor price a person set, it waits twelve hours before touching the same product twice, and it never plays match-the-lowest, because two robots doing that race each other to zero profit in an afternoon.

It’s made over 138,000 price changes so far, every one written down with the reason, so “why is this listed at $41.20?” takes one look, not an argument.

Where it started

The first thing I built at RMH3 was a Python and Flask tool that scraped competitor prices with XPath and pushed updates through the marketplace API. It replaced the spreadsheet and it worked, but it lived beside the business instead of inside it, and as the ERP grew it became the second place prices got decided.

The schedule

Every product/vendor pair gets a row in a schedule table: an anchor time, a 12-hour cooldown, and the last price we pushed. A sweep runs every minute and only touches the rows that are due. For each one it scrapes the live market, runs the strategy, and pushes only if the answer changed and the cooldown has cleared. Every attempt, push or hold, lands in a log with the reason and the competitor prices the engine saw. That log is the most-read table in the system.

The strategy

“Match the lowest” is what everyone asks for and nobody actually wants; two sellers running it converge on zero margin in an afternoon. The tiered strategy instead:

  1. Finds the buy-box winner in the mode we care about (authorised sellers by default).
  2. Undercuts them by a configured step, but only down to the product’s floor.
  3. Holds if the floor would be breached, if the winner is us, or if nothing has moved since the last push.

Floors and ceilings are per product and set by a person. The engine never invents a floor.

One engine

The rule that prevents the most bugs is boring: exactly one function computes a price, and every path calls it. The manual Execute button, the bulk push, the ⚡ Auto toggle, the recompute after a scrape, and the “next run” preview on the listing page all go through the same code. If the preview says $41.20, the job pushes $41.20. Before this the dashboard showed an “optimal price” computed one way and the job pushed a price computed another. They agreed most of the time, which is the worst amount: often enough to be trusted, rarely enough to be caught.

Health, not just price

The same engine feeds a buy-box analysis every five minutes, so the intelligence feed can say which listings lost the box, which competitors are new, and where our own floor is what’s holding us back. Overrides, contract pricing and promos stay manual on purpose. Those are judgment calls; the engine’s job is to make them visible, not make them.

Every push, with its reason

The repricing log: 8,413 pushes over seven days, each row showing the product, the old and new price, the percentage change, the trigger source and the reason the engine moved.
Seven days of the log — 8,413 pushes across 169 pages. Each row carries the reason it moved, including the cases the engine declined to chase: 'deferred_sibling_reserve: primary twin has a fresh 12h window — holding this account's down-move in reserve to retake if re-undercut'. Product names and the second storefront's name are blurred.

Nothing moves a price without writing down why. That’s what turns a wrong price into a five-minute question instead of an afternoon.

What I learned

  1. Two engines that "should" agree will diverge, and you find out from a customer. The fix isn't syncing them. It's deleting one.
  2. A cooldown is a feature, not a limitation. Repricing on every scrape turns two sellers into a price war with a one-minute tick. A per-product anchor with a 12-hour hold made prices stable enough for the buyer to trust.
  3. Log every attempt, including the ones that decided not to push. When margin drops, the question is always "why didn't it move?", and "it ran and chose to hold" is an answer only if you wrote it down.
GET/api/work/{id}

Parameters

Request

curl "https://portfolio.dylansg0318.workers.dev/api/work/internal-erp/repricing?fields=learned"
source: src/content/projects/{id}

Response200 OK · 681 B

{
  "learned": [
    "Two engines that \"should\" agree will diverge, and you find out from a customer. The fix isn't syncing them. It's deleting one.",
    "A cooldown is a feature, not a limitation. Repricing on every scrape turns two sellers into a price war with a one-minute tick. A per-product anchor with a 12-hour hold made prices stable enough for the buyer to trust.",
    "Log every attempt, including the ones that decided not to push. When margin drops, the question is always \"why didn't it move?\", and \"it ran and chose to hold\" is an answer only if you wrote it down."
  ],
  "page": "https://portfolio.dylansg0318.workers.dev/projects/internal-erp/repricing"
}

Live: this is the answer from this site’s API, not a mock. Send re-runs the request with your parameters; if the answer names a page, it opens.