undercut
Log in
europe 7 373 gold 4 567 fresh 106 302 aging 21 479 stale Grandmaster's Veilweaver Mandibles 122 999 now Grandmaster's Veilweaver Carapace 259 999 now west 7 743 gold 2 022 fresh 61 111 aging 28 035 stale Grandmaster's Imbued Leather Folds 4 000 now Master's Humming Avalonian Whirligig 16 111 2m east 12 627 gold 469 fresh 20 337 aging 11 558 stale Adept's Rune 7 now Adept's Relic 421 2m

Patch notes

The full technical record. For the plain-language version, see what's new.

Undercut — patch notes

The full technical record, newest first. Served publicly at the site's /patchnotes; the plain-language player version lives in whatsnew.yaml (/whatsnew). Release ritual mirrors regear/docs/RELEASING.md: bump VERSION in app.py, add a dated entry here, and (for user-visible changes) a whatsnew.yaml entry — all in the same PR. Don't put anything here you wouldn't post publicly.

v0.18.0 — the polish train — 2026-07-11

Transmutation, eighth car — and a silent /craft/top distortion fixed on the way. Research surprise: the dumps model transmutes as ordinary alternative recipes (flat @silver fee + a TRANSMUTE craft-button override), and the parser was ingesting them while silently dropping the fee — so live /craft/top has been ranking tier-ups/enchant-ups as FREE conversions. Now: staticdata parses silver_cost + transmute onto recipes (schema migration, fixture-tested), /craft/top excludes transmute recipes, and the new /transmute board ranks every conversion — buy the source, pay the fee, sell the target (calc.transmute_profit, golden-tested; no returns, no focus) — with the full integrity kit: capped asks, proven-first, Vol/day, freshness chips on both legs, top-7 behind the counter, free tail cap. Fees come from the weekly static refresh, not a hand-copied wiki table, so balance patches update them automatically. Farming/alchemy economies stay research items on the roadmap.

The snappy pass, seventh car — measured first, then three fixes the numbers asked for. Measure: a timing middleware stamps Server-Timing on every response and logs anything over 300 ms (no APM). Fix 1: /craft/top's rank_crafts (the one CPU-bound request path) joins the boards' 60 s cache — stock-knob combinations only (bool toggles × tier × sort × city × region is a bounded key space; a user-typed fce/fee would grow the cache dict without bound, so personalized knobs recompute per request as before). Fix 2 — attempted, measured, rejected: the partial index the roadmap earmarked for the boards' fresh-CTE scans (flip parallel-seq-scans the whole 360 MB prices_current heap, 435 ms of 665 ms). Built it CONCURRENTLY on live (a plain build under ingest's HOT updates came out indcheckxmin — planner refuses those), then measured: the ~30k fresh rows scatter across the heap, and on Longhorn's random-read profile the bitmap heap scan ran 1.5–3.1 s vs 556 ms for the seq scan it was meant to replace. Dropped; schema.sql keeps the negative result as a comment so it isn't re-tried blind. Fix 3: jit=off on the app pool — JIT was compiling 120 ms of machine code per flip query to return 6 k rows.

One account, two doors, sixth car. A Discord login and an email login used to be two unrelated accounts; /account now has a "Sign-in methods" card that links them. Discord-only accounts add email+password (POST /account/email, same validation as /register); email accounts click "Connect Discord" — the OAuth state payload carries the link intent (auth.make_state(link=True) / read_state; pre-deploy plain-string states still accepted), and the callback attaches the identity to the live session instead of logging in (auth.fetch_discord_identity split out of handle_callback, no users-table write). Both directions are one guarded UPDATE (queries.link_discord / add_email_login): only onto an account missing that door, only an identity no other account owns — race-safe, no transaction. Link-only v1: merging two EXISTING accounts (watchlists, presets, plans) is deliberately out of scope.

Copy link grows up, fifth car. The button was on two pages (craft, flip) with no explanation of what it copies; now every tool page has it in a consistent spot — result header next to the watch button (craft, refine, item) or right under the board's summary line (flip, haul, blackmarket, craft/top) — and a title spells out the intent: the URL carries city, filters and your prices, so the link IS the calculation. No mechanics changed (still location.href, still no deps).

The calculators get personal, fourth car. Two assumptions dropped: that you craft and sell in the same city, and that every material comes off the market board. Sell city — craft and refine forms grow a "Sell in" select (defaults to the craft city, so existing URLs and presets are untouched); _craft_with_prices takes a separate sell-price map, the by-city explorer deliberately stays same-city ("where is this craft best"). Your prices — every material row gains an editor (px_<ITEM_ID> query params via the native form= attribute, no JS): serious crafters buying from trade discords type what they actually pay, overrides win over the market, also fill market holes, and wear a brass "your price" badge instead of a freshness dot (a user's number is not a market observation — DESIGN rule 2 respected, not faked). Overrides ride in the URL: Copy link and presets carry a fully personal calculation. Dead _craft_in_city helper deleted.

Outlier defense on What-to-craft, third car — and the answer to "why is the board all capes". Three structural holes let one manipulated listing own the ranking: no volume weighting, price_suspect silently passing anything without a 7-day baseline (exactly the thin artifact markets), and a lone RMT ask setting the sell price. Now: product asks above suspect_ratio × the market's own avg_7d are re-ranked instead of hidden — a flagged ask is a flag, not a price, so the row ranks as if selling at the week's average (calc.capped_ask, golden-tested; ranking at the 3× ceiling still let a pumped listing own the board), stays visible, and wears an "ask held" badge disclosing the real ask; recipes with no weekly trade evidence sort after proven ones (stable partition) and wear "unproven"; a Vol/day column (from price_baselines.daily_volume, now returned by sell_price_map) shows the throughput evidence. Suspect input prices still skip the recipe — a cost built on a manipulated ask lies both ways. CSV export gains daily_volume/proven/ask_capped_from.

Gates + grades, second car.

The top-7 lock reaches every ranked board. PRO_TOP_ROWS 5→7, and /craft/top — which redacted nothing (free saw the top 20 openly, only the tail capped) — now holds its best rows behind the counter like flip/haul/blackmarket: podium cards and table rows keep their numbers but identity (name, icon, link) is never rendered, option-B style. The CSV was already Pro-only.

Scores wear letters. calc.score_grade buckets trade_score into S+/S/A/B/C/D (log-ish, ~×3 a step, golden-tested); flip/haul/blackmarket render a .c-grade badge (styleguide'd) instead of a seven-digit silver/day figure that read like a bug. The column header tooltip explains the letters; raw scores still order the board server-side and stay in the Pro CSV exports.

Polish pass, first car of the v0.18.0 train.

Region switcher actually switches now. The nav <select>'s inline handler did new URL(location) — but inline handlers resolve bare identifiers through the element→document→window scope chain, so URL hit document.URL (a string) first and threw URL is not a constructor. The cookie write before it succeeded, which is why the next navigation always showed the right region and nobody noticed the missing reload. Reproduced live with agent-browser; fix is new window.URL(...).

Status page stats align. .c-stat is inline-block and sat on the default vertical-align: baseline, so a card whose label wraps (ledger table names) or carries a freshness chip (gold) pushed its big value upward relative to its neighbors. Now vertical-align: top.

Ticker under prefers-reduced-motion becomes a hand-scrollable strip (overflow-x: auto) instead of a frozen bar. (Owner's "ticker doesn't move on my PC" was this media query honoring the OS animations-off setting — kept, it's correct a11y.)

Identity for shares and tabs. Hand-written static/favicon.svg (geometric brass "u" + the live freshness dot) with an apple-touch-icon and a /favicon.ico route (PNG bytes; every consumer sniffs). Default og:image — a 1200×630 Counterweight card rendered from devtools/brand.html with the real vendored fonts (agent-browser element screenshot, no new deps) — so any URL pasted into Discord unfurls with a branded graphic; twitter:card upgrades to summary_large_image except on craft-with-item pages, which keep their square item-icon summary.

v0.17.0 — the shareable release — 2026-07-11

SEO/OG groundwork. robots.txt and sitemap.xml are routes (both need PUBLIC_BASE_URL); the sitemap lists the 11 static public pages only — /item/{id} is deliberately absent, thousands of thin pages better reached through internal links. base.html gains the head block: meta description (overridable per page), path-only canonical, og:/twitter tags. Canonical strips the query string on purpose so /craft?item=… variants collapse into one indexable page; og:url keeps the query so a scraper re-fetch of a shared link lands on the result, not the blank calculator.

Shareable permalinks. Craft results already lived entirely in the URL ("a URL IS a saved preset") — they now carry result-aware titles and descriptions plus an item-icon og:image, so a pasted link unfurls with the item, city and profit (loss copy is honest: "the math said don't"). Flip's description is aggregates-only by constraint: _lock_top redacts row identities in the template, so naming flips[0] would hand the Pro rows to every unfurl. Both pages get a Copy link button (_macros.copy_link, navigator.clipboard, no new deps).

Watchlist overnight. Logged-in, the landing hero's movers floor becomes "your watchlist overnight": new queries.watchlist_overnight extends the watchlist LATERAL with a second one against price_history (scale 6, the cheapest city's OWN bucket ~24h back, 48h floor so sparse markets don't diff against week-old data), move computed in SQL — ::float before the divide, because bigint/bigint quietly truncates every move to 0 — biggest movers first, per-user and uncached (cache-key discipline). EXPLAIN on the live db: both LATERALs are index walks, the history probe a backwards PK scan, 0.7 ms. Logged-out landing is byte-identical to before; logged-in with an empty watchlist keeps the movers floor plus a hint line. seed_db.py now writes scale-6 buckets so screenshots show real deltas.

Tech debt: new --radius-lg token (10px) replaces the ad-hoc 8/10/12/9px radii on navlink/menu/floor/tile. Screenshot matrix light+dark passed with no overflow; suite + ruff green.

docs: roadmap consolidation — one living plan — 2026-07-11

Plans were smeared across five docs, two already stale. New docs/ROADMAP.md is now the ONLY place plans live: vision, owner handoffs, pending decisions, now/next/icebox, launch gates, done-log, doc map — with the standing rule (also in CLAUDE.md) that any PR shipping/deferring/ deciding a board item updates it in the same PR. Retired: docs/BIG-UPDATE.md (fully executed 2026-07-10; git history keeps it — its Ch. 0 dev-box operating manual moved into devtools/README.md, which also loses its docker-only recipe: this box runs the throwaway Postgres as a cluster pod) and docs/PRODUCTION.md (absorbed under Launch gates). MARKET.md drops its embedded roadmap/retention lists for pointers and stays the competitor reference. No code changes.

docs: marketing plan — Google Ads + social posting — 2026-07-11

New docs/MARKETING.md: the phased promotion plan for undercut.market. Organic first (Reddit/forum/Discord launch posts + a weekly data-post engine fed by the site's own numbers), then a capped €5-10/day Google Ads search test with exact/phrase keywords, negatives, and a weekly search-terms loop. Paid conversion tracking is deliberately deferred: the site ships zero third-party scripts, so gtag would force the GDPR consent banner — the test measures via URL params + logs instead, and the banner only gets built if the test earns a real budget. Draft ad copy and the launch post are written through the human-copy pass and lint-checked. No code changes.

docs: beta access model options + no-KVK payment routes — 2026-07-10

PRODUCTION.md gains §1b: the three ways to run the pre-payment beta (free Pro for accounts / everything open / gates + waitlist) with trade-offs, recorded for a later owner decision — the live gates currently point at a counter that can't take money, and that tension is now written down instead of ad-hoc. Also records the Stripe-needs-KVK finding and the merchant-of-record fallback (Paddle/Lemon Squeezy) for selling without a Dutch business registration. No code changes.

ops: domain cutover to undercut.market — 2026-07-10

The owner bought undercut.market and wired Zoraxy + DNS; the site serves there with TLS. PUBLIC_BASE_URL flips from the staging domain to https://undercut.market — this drives the Discord OAuth redirect (/auth/callback), verification-mail links, and the (future) Stripe webhook URL. The matching redirect URI must be added on the Discord application in the same window. No code changes.

v0.16.1 — Daily bonus, told straight — 2026-07-10

BIG-UPDATE Ch. 6 asked for auto-applied daily bonuses from an authoritative rotation. Research verdict: there is no rotation to transcribe — the game draws two random categories at server downtime (multiple wiki/forum sources agree), shows them only in the in-game Activities window, and neither ao-bin-dumps nor any API exposes the draw. Auto-apply would mean fabricating data, so the checkbox stays manual and now explains itself: a tooltip on the craft/refine "daily bonus" checkbox says how the mechanic works and where to look. constants.yaml documents the finding; the 10%/20% magnitudes keep their [VERIFY] marks until the owner confirms them in-game.

docs: Big Update research chapters — 2026-07-10

The three research deliverables from docs/BIG-UPDATE.md, no code changes:

v0.16.0 — Trade-quality scoring + the counter gate — 2026-07-10

The scans ranked by absolute unit profit, so the free top-20 filled with technically-tradable but practically-dead markets (BIG-UPDATE Ch. 2b). Now they rank by what a trade can actually clear.

Data: flip/haul candidates gain buy-leg (source city) volume, and blackmarket_candidates gains its first liquidity evidence (source volume + average) — all cheap price_baselines joins, no LATERALs. freshest_movers also folds in the R2 deferral: suspect prices no longer make the landing tiles or ticker marquee.

Score: pure calc.trade_score(unit_profit, buy_volume, sell_volume, age_s, baseline_distance) ≈ realizable silver/day — unit profit × the thinnest KNOWN leg's daily volume, halved per 6 h of price age, discounted by distance from the priced leg's own 7-day average. Unknown volume doesn't bind (unknown is not zero); known-zero binds hard; nothing-known scores 0. Golden tests. Flip/haul/BM sort by score; each row shows it in a Score column with a plain-words tooltip; the flip CSV exports it.

Pro gating — option B: free (and logged-out) users see the whole ranked board shape, but the top 5 rows' item/cities are redacted server-side (block glyphs — the real strings never reach the HTML; the flip hero card redacts too, including the item render). Numbers stay visible: the pitch is "the best trades are behind the counter", not a blank page. PRO_TOP_ROWS = 5; the .c-gate card and /billing copy updated to match; craft-top keeps its plain top-20 (it's a crafting ranking, not a trade board). New .c-lockedrow + .u-redact on /styleguide.

v0.15.0 — The price-integrity layer — 2026-07-10

One manipulated listing used to poison everything downstream of it: craft costs, ROI rankings, alerts. AODP ships only sell_price_min, so a lone 50x listing IS the minimum. v0.12's spread_flags guarded the flip/haul display only; the calculators consumed raw prices unguarded. This release builds the one choke point (BIG-UPDATE Ch. 2a):

Evidence — new price_baselines table: per-(region, item, city) 7-day average price + daily volume, refreshed by ingest after each daily-scale history sync (+ 2-day prune so dead markets read as no evidence). The flip/haul dest-leg LATERALs over 9M history rows are gone — they read the same numbers from the table now.

Verdict — pure calc.price_suspect(sell_min, avg_7d, max_over, max_under) -> None | 'inflated' | 'crashed', spread_flags generalized. Bound = price_integrity.suspect_ratio (3.0) in constants.yaml — deliberately looser than the flip display bound: excluding an input silently breaks a whole recipe, so only clear manipulation qualifies. No baseline = no verdict — a thin market keeps its only honest price. Golden tests.

Consumption per surface: - Calculators (craft / refine / craft-top): suspect = MISSING via one _hold_suspect gate — the existing missing-price degradation paths are honest. rank_crafts already skips recipes with holes. - By-city tables: the suspect city's row drops, with a muted "suspect price held back in …" note naming the cities. - /item price grid: never hides — the price wears a suspect ask warn chip (quality-1 rows, where the baseline evidence applies). - Alerts: _fire_alerts ignores suspect prices — a manipulation spike must not trigger everyone's "climbed to X" alerts. No baseline = alert still works. - Flip/haul/BM: display logic unchanged, evidence now from the table.

Deploy order: price_baselines is applied to the live DB before this deploys (idempotent DDL, also in schema.sql for ingest's startup apply), so the web never queries a missing table.

seed_db grows three demos: a manipulated product (T4_2H_CROSSBOW Martlock 12x), a manipulated input (T4_METALBAR Thetford 15x — poisons every metal recipe there), and a crashed bait listing (T4_BAG Bridgewatch /8).

v0.14.1 — Landing perf hotfix — 2026-07-10

v0.14.0's tradeworthy guard on freshest_movers (an EXISTS-OR in the WHERE) pushed the live planner into a per-item nested loop over prices_current: 8.5 s per call, on the landing page's request path and in the minutely ticker refresh for all three regions. Two fixes, measured on the live DB:

v0.14.0 — Owner-testing fixes — 2026-07-10

The five bugs from the owner's testing pass (docs/BIG-UPDATE.md Ch. 1), plus two small design-debt items from Ch. 8 that ride along.

Region switcher works logged-out. The nav select now writes a plain region cookie (unsigned — worst case a forged cookie shows another server's prices) and resolve_region reads it between the ?region param and the account default. The select's own fallback chain reads the cookie too, so pages without a region context (billing, account) show the right selection. Logged-in behavior unchanged; /settings/region still writes the DB default.

Item charts stop drawing time backwards. The Chart.js category axis ordered labels by first appearance across datasets, so a date only a sparse-coverage city had got appended after the dense city's last label — non-monotonic axis, zigzag lines. item_page now ships one sorted union of dates as labels with every city's series aligned to it; missing days are honest null gaps (spanGaps: false). While in there, the city palette now follows DESIGN.md rule 10: brass for the highlighted series (Caerleon), ink neutrals told apart by dash pattern for the rest — profit/loss hues no longer decorate non-semantic series.

Raw item ids stop leaking into names. freshest_movers — the one scan query without the v0.12 tradeworthy guard, feeding the two most visible surfaces (landing tiles, ticker movers) — now carries the same recipes-or-recipe-inputs EXISTS predicate. Item search sinks unnamed rows (name_en = id) below real matches. Watchlist and alert rows keep raw-id items the user added deliberately but render the id in the data mono, so it reads as an id.

Blank item renders hide themselves. render.albiononline.com has no image for token/skin ids; both <img> sites (the iframe() macro, the landing tiles) get an onerror that leaves an empty inventory frame instead of a broken-image icon.

"Flip the spread" explains itself. The landing card now says what a spread is and that the tool finds it: "The same item, two cities, two prices. This finds the gaps still worth it after the round-trip tax."

Light-theme freshness chips darkened for contrast (Ch. 8 debt): --fresh

1E7C4D→#186B41, --aging #9A6A14→#85590F — the chip renders at 12 px and

was ~4.3:1 against --bg; both now clear 4.5:1 on both surfaces. Dark theme untouched; DESIGN.md palette table updated in the same PR.

docs: the Big Update brief — 2026-07-10

New docs/BIG-UPDATE.md: the self-contained implementation brief for the next major update, written for the agent that will execute it. Contents: an operating manual (env quirks, the agent-browser loops, byte-diff gate, release ritual); five root-caused bugs from owner testing (anonymous region switcher has no persistence; item charts break on Chart.js category-axis label union; raw item ids leak through freshest_movers — the one scan query missing the tradeworthy filter; blank icons lack an onerror; the flip counter-card copy); the price integrity layer (suspect-price baselines so one troll listing stops poisoning craft costs/ROI/alerts); trade-quality scoring for the scan rankings + Pro gating options; research chapters (Pro scanner client with TOS analysis, production-readiness checklist, retention drivers); the real daily-bonus rotation data task; and the deferred/roadmap carry-overs. No code changes.

v0.13.0 — The front counter — 2026-07-10

The design pass the foundation release made room for: the nav, the pro surfaces, and the four scan pages each get a designed front-of-house.

Nav. Desktop groups the ten flat links into Craft ▾ (calculator / refine / what to craft) and Trade ▾ (flip / haul / Black Market) plus Gold and a brass Pro link — native <details> disclosures (click/tap/keyboard everywhere; a 4-line script adds outside-click dismiss), NOT the Popover API (positioning support risk) and NOT hover menus (iOS never focuses buttons). The signed-in right side becomes an account menu (Account / Watchlist / Alerts / Log out). Status moves to the footer. Mobile keeps the flat swipeable strip; the strip⇄groups switch sits at 920 px, undoing v0.12.0's 1279 px stopgap since the grouped bar measures ~800 px. New .c-menu component on /styleguide.

/account (new). Plan card with usage meters (.c-meter, watchlist 7/10-style), the region default finally gets a read surface (posts to the existing /settings/region), sign-in identity, and — for Pro subscribers with a Stripe customer — POST /billing/portal into Stripe's billing portal (degrades to receipt-email copy when the portal isn't available).

/billing redesign. The bare comparison table becomes two .c-plan pricing cards — Free ("0 silver, ever") and Pro with the counter's brass top edge, profit-green checks, and an optional PRO_PRICE_TEXT env for the display price — with the detail table demoted below.

The Pro scan gate (the model every paid competitor converged on: free = check things one at a time, paid = the ranked board). Free accounts see the top FREE_SCAN_ROWS = 20 of each ranked scan (flip, haul, Black Market, what-to-craft); a .c-gate card under the table counts what's behind the counter. Calculators (craft/refine) are never capped; CSV stays Pro.

The scan pages, redesigned. Each keeps its data density and gains an identity: flip opens with a best-spread hero (.c-bestrow + .c-route buy→sell legs, both chipped); haul frames the route ("Martlock ⟶ Caerleon", capacity, a red-zone badge replacing the old flash banner, best-trip stat); Black Market gets its shadowed header card ("the buyer in the dark"); what-to-craft podiums the top three (.c-podium, itemframes + profit + ROI) above the rest of the table. All four tables now sit in cards. /craft opens populated (CRAFT_DEFAULT_ITEM = "T4_BAG") instead of an empty form — the "craft looks boring" complaint was really "craft is empty until you type".

Dev. get_user now returns discord_id/email/stripe_customer_id; shoot.py shoots /account and a logged-out /billing.

v0.12.0 — The foundation — 2026-07-10

Mechanics release before the design pass: page speed, data honesty, the sweep deadlock, the frozen ticker, and a new browser-automation harness.

Performance. Live TTFB was 1–2.3 s on the heavy pages; the budget is now <400 ms. Three moves, no new infra: (1) the ticker's TTL dict generalized into cached(key, ttl, fetch) (60 s on the flip/haul/BM candidate scans — within the scanner's own 120 s cadence, and every price still wears its freshness chip — 1 h on the static recipe list; keys only ever contain clamped params, see the _CACHE comment). (2) The /refine chain (~36 sequential round trips: 4 queries × 7 tiers + a 7-city compare) and /craft (~13) now batch: items_by_ids + recipes_with_inputs_many + one all-city sell_prices_with_age_cities fetch, with _craft_in_city split so _craft_with_prices computes from the pre-fetched map — 4 queries per page. (3) /item fetches its 7-city history in one history_for_item_cities query and gathers the rest; /, /status, /craft/top gather their fan-outs. Post-deploy measurement found the last ~0.9 s on / and /status: the ledger-size count(*) over ~9M price_history rows — now behind the same 60 s cache.

Data sanity — the 72,436% flip. The flip page could pair a real cheap sell with a lone manipulated ask (the dear leg had no bound at all; live DB showed 25 items with a >100× cross-city spread, worst 12,345×). New pure calc.spread_flags(sell, dest_avg_7d, dest_daily_volume) flags a dear leg that asks >2× the destination's own 7-day average ('outlier') or has no recorded sales there ('no-market'); wide spreads against a real market stay — hauling exists because cities disagree. The candidate queries also mark items outside the crafting economy (tradeworthy: in recipes or recipe_inputs). Flagged rows are held back with a count + "show them anyway" link (?all=1 renders them wearing a .c-badge--warn naming the reason); the Black Market dear leg is the game's own standing buy order, so only the tradeworthy check applies there. Haul gained the same guard plus a Vol/day column. min_price moved from the flip SQL into Python (bounded cache keys).

The sweep deadlock. price_sweep and the NATS consumer upsert overlapping (region, item_id, city, quality) rows; batches landed in AODP-response order, so the two writers took row locks in different orders and Postgres killed one every few minutes (DeadlockDetected on /status). Every upsert batch (sweep, NATS, history) is now sorted by its conflict key before executemany — consistent lock order is the actual fix — and v0.11.0's prune-only deadlock retry generalized into _retry_deadlock around all of them. The freshness-guard WHERE on PRICES_UPSERT is untouched.

/status degrades per card. A missing table used to 503 the whole canary page through the global DBError handler; each card now degrades alone (_card → a muted "this ledger is missing" note) and the cards fetch concurrently.

Ticker. The track was width: max-content with ~6 short items — narrower than an ultrawide viewport, so the -50% animation was imperceptible and the bar sat mostly empty. The two content passes are now explicit .c-ticker__sets with min-width: 100vw (a set always fills the bar; the -50% keyframe is exactly one set, so the loop is seamless), and the ticker carries more market: each region's two freshest movers, linked. Reduced motion still gets a static, now full-width, bar.

Icons. <link rel=preconnect> to render.albiononline.com, and the icon host is now the ICON_BASE Jinja global (env-driven) — devtools point it at /static to run offline, and a caching proxy can swap in without template changes.

Dev. devtools/shoot.py now drives the agent-browser CLI instead of Playwright — same viewport matrix, same overflow --assert gate (via eval), sessions via the existing /dev/login//auth/logout instead of minting cookies. The agent-browser skill is installed repo-level (.agents/skills/) for all future UI work. seed_db.py seeds history for every item/city tracking each city's price (±10%) plus three deliberate flag demos (a 900k outlier ask, a no-history artifact, a no-recipes token). New undercut/docs/MARKET.md: the competitor landscape + prioritized roadmap.

v0.11.0 — The counter — 2026-07-10

A full front-of-house rebuild, plus the fix for the failed rows that were sitting on /status.

The shell. The top nav is now sticky and translucent (backdrop blur), with a brass edge and the wordmark's undercut-step reused as the active-tab marker; on mobile the tools collapse to a swipeable pill strip. A live market ticker sits under the nav on every page — each region's gold price and fresh/aging/ stale counts, refreshed in-process at most once a minute (get_ticker, TTL cache; attach_ticker HTTP middleware skips static and htmx requests).

The landing. New hero: "freshest on the floor" tiles (freshest_movers query — one row per item, freshest first) rendered in Albion inventory-frame thumbnails (.c-itemframe), a tool-card grid, and the floor stats. The old centred 1200 px column is gone: a .page CSS grid gives content / wide / bleed tracks, so the wide multi-column tables (flip, craft/top, haul, blackmarket) break out and the hero bleeds full width. Motion is one compositor-only page-load reveal plus native cross-document View Transitions, both under prefers-reduced-motion.

Performance. GZipMiddleware (pages were shipping uncompressed — /status dropped 8.6 KB → 1.8 KB on the wire), immutable long-cache on /static (fonts + htmx + Chart.js stop revalidating every navigation; style.css is cache-busted with ?v=VERSION), and <link rel=preload> on the two woff2.

Ingest reliability — the /status failures. The pool was the psycopg default (max 4) against ~18 always-on ingest coroutines, so a boot-time stampede timed out and pinned FAILED rows on /status until each task's next (up to weekly) run. db.py now sizes the pool from DB_POOL_MAX (ingest Deployment sets 16) and validates connections on checkout (check_connection) to survive a CNPG blip. prune retries on deadlock against the live writers, and the long-interval tasks (history, prune, static) retry in 5 min on failure instead of waiting out their full interval.

Dev. devtools/shoot.py gained a --assert horizontal-overflow gate (fails with selectors), covers every page, and aborts off-origin requests so the loop runs offline. Added the human-copy skill and a read-only design-review agent.

v0.10.0 — The till — 2026-07-10

Phase 5 of the launch plan: the pro tier. Everything ships dark until the Stripe keys exist (stripe-secret-key / stripe-webhook-secret / stripe-price-id in undercut-secrets, all optional: true in the deployment) — /billing says "not purchasable yet" until then.

v0.9.0 — Keys and tripwires — 2026-07-10

Phase 4 of the launch plan: email accounts + price alerts.

v0.8.0 — The week's rhythm — 2026-07-10

v0.7.0 — The exchange board — 2026-07-10

v0.6.0 — The wire — 2026-07-09

v0.5.0 — The fence's ledger — 2026-07-09

v0.4.0 — The wagon ledger — 2026-07-09

v0.3.0 — The spread scanner — 2026-07-09

v0.2.0 — The refinery opens — 2026-07-09

v0.1.1 — Ingest breathes — 2026-07-09

First contact with production AODP found two launch bugs in the ingest worker; no data had landed yet, so no migration needed.

v0.1.0 — The counter opens — 2026-07-09

Initial release: the full platform (Phases 0–2 of the launch plan) in one PR.