Pricing Guardrails and Safety Controls
Navgold applies layered safety controls before any price or stock reaches Amazon or Repricer.com. Every control is configured on the Navgold Settings singleton and enforced in code — most of them at the outbound chokepoint, so a block happens before any HTTP call leaves the system.
The outbound chokepoint
flowchart LR
A[Refresh strategies<br/>compute min/max] --> B[Build feed<br/>build_pricing_feed]
B --> C{Guards}
C -->|freeze on| X1[BLOCK<br/>AmazonFrozenError]
C -->|Dry Run| X2[BLOCK<br/>PricingNotApprovedError]
C -->|deviation too far| X3[SKU dropped from feed]
C -->|price <= cost / min>max| X4[SKU flagged, not published]
C -->|all pass| D[Feeds.create_feed_document<br/>Feeds.create_feed]
D --> E[Amazon SP-API]
Feeds.create_feed_document, Feeds.create_feed and RepricerClient.post_repricer_data each call the freeze + approval guards before the transport call, so a frozen or un-approved system never emits a request.
Controls at a glance
| Control | Navgold Setting(s) | Default | What it does |
|---|---|---|---|
| Freeze / kill-switch | freeze_amazon_outbound |
Off | Hard-blocks all outbound (feeds + repricer) at the chokepoint |
| Loss circuit-breaker | enable_loss_circuit_breaker, loss_circuit_breaker_threshold (GBP) |
Off / — | Auto-sets the freeze when below-min losses in a rolling window exceed the limit |
| Dry-Run / Live approval gate | pricing_sync_mode (Dry Run / Live), enable_pricing_sync |
Dry Run / Off | Dry Run renders a preview and never pushes; going Live requires explicit approval |
| Price-deviation band | price_change_threshold_percent, price_change_block_direction |
20% / Both | Drops a SKU from the feed if its new price moves too far from the current live price |
| Below-cost floor & band | (per-strategy min/max) | — | Never publishes a price at/below purchase cost, ≤ 0, or with min > max |
| Oversell / stock reserve | stock_reserve_pct, stock_publishing_mode |
0% / Publish Actual Stock | Holds back a % of stock, caps quantity, and can force zero-stock publishing |
| Refresh-stagger guard | (internal, pause_strategy_refresh) |
Off | Never submits a feed built from half-refreshed prices |
Freeze / kill-switch — freeze_amazon_outbound
The master stop. When on, block_if_amazon_frozen() raises AmazonFrozenError inside Feeds.create_feed_document, Feeds.create_feed, and the Repricer client — before any request is built. A blocked submission is logged as Skipped (not a failed sync), so it never pollutes the failed-sync triage queue.
Loss circuit-breaker — auto-freeze on real losses
An automatic version of the kill-switch. On every Sales Order below its minimum price, check_sales_order_below_min records the loss into a rolling time-window (Redis) and calls trip_circuit_breaker_if_needed(window_loss):
- If
enable_loss_circuit_breakeris off → do nothing. - If
loss_circuit_breaker_threshold≤ 0 or the window loss is below the threshold → do nothing. - If already frozen → do nothing (idempotent).
- Otherwise → set
freeze_amazon_outbound = 1and alert.
A scheduled sweep_circuit_breaker() re-checks recent below-min sales independently, so a burst that slipped past the per-order path still trips the freeze. Thresholds are in GBP.
Dry-Run / Live approval gate — pricing_sync_mode
Outbound pricing runs in Dry Run by default:
- Dry Run — the scheduled sync renders the exact feed a live push would send and stores it as a preview + variance report, then stops.
block_if_pricing_not_approved()raisesPricingNotApprovedErrorat the chokepoint. Nothing reaches Amazon. - Live — requires explicit approval (
approve_and_go_live, System Manager only), which stampspricing_sync_approved_by/_at.revert_to_dry_runflips it back and re-blocks all outbound.
enable_pricing_sync is the master switch above this — with it off, no scheduled submit runs at all.
Price-deviation band — price_change_threshold_percent + price_change_block_direction
Guards against a spiked/crashed price shipping. A SKU whose new price deviates from its current live selling price by more than price_change_threshold_percent (default 20%) is dropped from the feed and recorded in blocked_price_deviation. The direction filter narrows it:
- Both — block large moves either way (default)
- Drops Only — block large drops, allow rises
- Increases Only — block large rises, allow drops
A SKU with no current selling price is blocked (fail-closed).
Below-cost floor & band integrity
Independent of deviation, the feed validators refuse to publish a price that is:
- at or below the purchase cost (below-cost leak),
- ≤ 0, or
- an inverted band (min > max, or min == max without a fixed price).
Flagged SKUs are excluded from the price feed rather than shipped.
Oversell / stock reserve — stock_reserve_pct + stock_publishing_mode
Stock safety before the quantity is published:
stock_reserve_pct— holds back a percentage of available stock (a negative reserve is clamped to 0; ≥ 100% publishes nothing).- Bundles — quantity is capped to the buildable qty from components; an unbuildable bundle publishes 0.
- FBA — FBA-fulfilled SKUs publish quantity 0 on the merchant channel (Amazon owns the stock).
stock_publishing_mode—Publish Actual Stock(default), or force Publish Zero Stock for all / selected items.- AFN safety — a failed Amazon inventory pull never zeroes existing stock (it aborts and alerts), so an API blip can't cause an oversell.
Refresh-stagger guard
Strategy refresh runs at :00/:30 and submits at :15/:45. If a refresh over-ran or never ran, the submit defers rather than shipping a feed built from half-refreshed prices. pause_strategy_refresh stops the scheduled refresh entirely.
Every control on this page is verified by automated tests that assert the unsafe path is blocked — see the Test Coverage & CI page under Engineering — not just the happy path.