← Hermes ⬇ PowerPoint
🐬 HERMES · v2.147.1 · hermes.appolis.app

HERMES

Hermes stopped reading the ledger.
It started keeping it.

A retention command centre for Shopify wellness brands. Five services, 157,536 customers, one screen — and a loyalty ledger it keeps itself, now checked against the whole book and found correct.

1,252
tests green
209
releases logged since 2026-07-24 (359 in the changelog)
100%
ledger correct — 59,916 linked members compared, 0 genuine mismatches
293,979
orders · 51,262 tickets · 284,179 shipments · 21,167 active subscriptions
5
services live — Shopify · Recharge · Gorgias · Rivo · Flexport
ShopifyRechargeGorgiasFlexportRivo🏛 Appolis ID
Derived from APP_BREAKDOWN.md §13 · book measured 2026-08-17
Scroll to explore
⚖️ The shift

Every channel. One person.

The old workflow scattered every customer across five logins. HERMES made them one person again — and put the whole company's surface behind one signed-in door. The app's own line for it, set in v2.34.1: “Every Channel. One Brain.”

🌪️
Before
  • Bounce Gorgias → Shopify → Recharge → Flexport → Rivo
  • Churn invisible until the cancel email lands
  • Refunds sent on the customer's word
  • No record of who saved whom
VS
🎯
With HERMES
  • 157,536 customer rows unified into one profile
  • Queue + VIPs served from queue_cache in ~200ms
  • The rebuild belongs to the cron — since v2.54.0 a request never recomputes it
  • At-risk flagged from their own words, before they leave
The real tab strip
hermes.appolis.app — signed in as overall admin
What an admin sees
At-riskVIPsSupport Hub📋 Board🎟 DiscountsInsightsAgoraMy FilesITTeam
What a non-admin sees
Support Hub📋 BoardAgoraMy Files
Studio retired v0.91.0 — the company's Phantasia brain is reached via the 🏛 Appolis button. “My Flippers” became Support Hub in v2.32.0, when one tenant's word for a customer came out of the routes, the cache keys and the tab ids.
The tab strip, recreated — and since v2.33.0 the 🏛 hub is the front door: an admin lands on the map, everyone else lands in their own department
🔌 Total integration

Five services, one bloodstream

Data flows in live — webhooks in seconds, full syncs on the cron. Every action flows back out on the platforms' own rails. Nothing is a copy; everything is the real thing.

🛒 Shopify293,979 orders · five webhook topics, registered v2.48.0
🔁 Recharge21,167 active subscriptions · full charge history · 71,906 subscription timelines backfilled
🐬 HERMESthe single brain · 284,179 shipments
💬 Gorgias51,262 tickets mined
🎁 Rivo66,990 loyalty events mirrored · 59,916 linked members
📦 Flexportsix webhook events, re-registered 08-13 — the Logistics API 2024-07 has no order-list endpoint
⚡ Webhooks land in seconds
🕰 Cron lanes: 30-min main · Rivo at (10,40) · loyalty heartbeat */5
✅ Actions run on the real platforms — never a shadow copy
hermes.appolis.app — IT · connector health
Connectors
🛒 ShopifyHEALTHY
sync 4m ago · hooks 24h: 312✓ 0✕
🔁 RechargeHEALTHY
sync 4m ago · hooks 24h: 88✓ 0✕
💬 GorgiasWATCH
sync 4m ago · hooks 3d: 1,215✓ 1✕
🎁 RivoHEALTHY
own cron lane (10,40)
📦 FlexportHEALTHY
webhooks + per-order reads
The IT board's connector strip — recreated from the live health tiles

v2.52.0 rebuilt what makes a tile red. It is now recency-gated — more than three failures and the last one inside two hours — because the old flat 24-hour count pinned red for 14 hours after a burst had ended and could never go green on a service handling 12,000 hooks a day. “Never synced” became its own amber state instead of reading as healthy forever, and the board is built from the service registry, so a connector whose settings row is missing shows as missing rather than as nothing wrong.

🎯 The daily surface

The queue is the product

Every morning starts on one list. A card only leaves it one way — the customer pays again. That turn is the whole app, and it is the only thing the design celebrates.

The flip
At risk · high
82
skipped their latest renewal 8 days ago
Saved
a save counts only when they pay again — free product never counts
The real QueueCard
hermes.appolis.app — At-risk · Churn risk
82
Michelle Steinweg
HIGHVIP★ cassyjewel
Skipped their latest renewal 8 day(s) ago
82
LTV $1,214
John Weber
SAVED
paid reorder confirmed — left the queue the only way it can
LTV $842
58
Julie Goolsby
MED
supply low, no order — ~4 days of product left
58
LTV $391
Left border = risk tier · ring = score · right rail = score over LTV — the live card, rebuilt
🧠 Only-here intelligence

Two engines nobody else computes

Both are ours, both run on our own orders and our own conversations — lib/ring.js and lib/risk.js. Neither is available off a shelf.

🧮

The replenishment ring

supplyDays = (quantity × servings_per_unit) ÷ dailyDose, counting down from delivery + a 3-day buffer. The dose is inferred: the median of servings ÷ gapDays across consecutive deliveries, clamped 0.1–5.0/day, needing at least two delivered orders. Tom under-doses at 0.68/day against a 1.0 label — so he stops getting nagged at the wrong cadence.

📶

The risk signal ladder

supply_depleted
30
shipment_stalled
25
first_order_stall
22
repeat_pauser
22
negative_open_ticket
20
renewal_skipped
20
subscription_paused
18
supply_low_no_order
15
lapsed_buyer
12
email_disengaged
8

Score = the sum, ×1.1 for top-decile LTV, capped at 100. High ≥70 · Med ≥45. A stalled shipment is structurally excluded from incoming supply, so it can never suppress the low-supply signals. 3,025 repeat skippers (2+ skips in 90 days) carry the flag.

hermes.appolis.app — Customer profile · Why flagged
🔥 In their own words: “this is a scam, I want my money back”
✅ Remedied recently — a ticket closed in the last 30 days with the customer sounding happy about the outcome.
Why flagged. Stalled shipment — “no subscription to churn, but a lost-package complaint (and their trust in the store) is in waiting if it does not move.”
shipment_stallednegative_open_ticketsubscriber: false
v0.98.1 — churn is a subscriber word. One-time buyers get the same urgency in the right words.
📦 Unique data nobody else has

Every customer's delivery story

Open any profile and see the shipping life they've lived with us — charted as bars or a line, split by carrier, split by warehouse era, incidents called out with one-click jumps into the order.

📈
Transit-time chart of their last 20 deliveries — colour-coded fast / okay / slow
10-31-25
ShipBob → Flexport cutover, learned from our own data — now the delivery report's date floor, so every modern order counts whoever shipped it
5.88d
avg doorstep time across 34,823 Flexport-era deliveries — subscribers vs one-time buyers, answerable in one report
🚨
Lost · stalled · never-arrived incidents surfaced per customer, per order
hermes.appolis.app — Customer profile · Delivery experience
📨 Last contact 06-19-26 email — THEY’RE WAITING ON US
Delivery experience · ∿ Line ▮ Bars By carrier Issues (1)
09-14-2511-02-2501-08-2603-26-2606-19-26
days from order to doorstep · ● ≤5d ● 6–9d ● 10d+ · dots: ● Flexport ● ShipBob (legacy)
A profile's delivery chart — contact headline on top, warehouse eras underneath

Every order view carries its own delivery analysis: this box vs the company average.

And the report tells you when to distrust it. v2.48.0 found that an unknown delivery date had been quietly recorded as the ship date, which made the affected rows average 1.9 days with zero late deliveries against the Flexport era's 5.88. An unknown delivery time now stays empty, and the page warns in amber when carrier data is missing and in red when a segment's transit looks impossibly fast — it declares its own worst figure untrustworthy rather than publishing a halved transit time as fact.

🔌 v2.48.0 → v2.48.1 · the honest post-mortem

“I don't know why everything got broken.

Tyler flagged the delivery report as “not reporting correctly at all”. It was not the report. Three faults were stacked underneath it, and finding them took measuring rather than reading.

What was actually wrong
🕳 A rename left orphans. On 2026-07-17 the app moved to a new workers.dev subdomain. Every Flexport and Recharge webhook was still registered against the old hostname.
📉 Recharge's five hooks simply failed forever — last event 07-14, the end of their retry horizon. Flexport eventually pruned all six of its own: asked for its list, it returned zero.
😶 And Shopify had never sent Hermes a single webhook. Not one, ever — confirmed twice, including by asking Shopify with the app's own credentials: 0 subscriptions owned by this app. So nothing had written carrier or shipment data for 17 days, and 16,759 orders carried no carrier at all.
🔧 Repaired at all three vendors. Six Flexport events re-registered and the six dead ones deleted; five Recharge subscriptions repointed in place, so the signing secret still matches; five Shopify topics registered for the first time.
Verified flowing, not assumed. The first Flexport events since 07-14 landed within the hour. Shopify turned out to be a firehose: 888 order updates and 234 fulfilment updates in about 80 minutes, and the first carrier stamps in two weeks appeared within two minutes.
✅ The pipeline is reconnected
The registration screen that did not exist
hermes.appolis.app — IT · webhook registrations
Registered to hermes.appolis.app
🛒 orders/create · orders/updated · fulfillments/create · fulfillments/update · customers/update5 OK
📦 Order.Packed · Shipped · Delivered · Cancelled · Return.Created · Return.Updated6 OK
🔁 Recharge — five subscriptions repointed in place5 OK
🐛 Found while fixing it. The Flexport registrar read the vendor's webhook list from a wrapper field the vendor does not send. The existing-hooks check could therefore never fire, and every press of the register button would have created a duplicate of every hook.
A subscription belongs to the app that created it and is signed with that app's secret — one made from any other tool would deliver and then fail verification forever.

The lesson has a name here now: a rename must audit every consumer of the old name. Webhook registrations live at the vendor, not in the repository, so they are consumers nobody sees — and they fail silently, for weeks, while every dashboard reads green.

📦 A real order, a real trap

The package that came back

Flexport sometimes re-ships a lost package under the SAME order — no new Shopify order, no portal order, just the prose “A replacement was sent”. The structured twin was hiding in the 2025-03 order payload.

KV
Kevin — order #278526the order that proved it · lost shipment → in-place resend · one-time buyer, no subscription
Three lanes, one order
1️⃣ Original shipment → shipped → CANCELLED — keeps status='lost' forever
2️⃣ A later sibling re-ships the same SKUs → kind='resend' + the story
⚠️ The trap: the resend reuses the original tracking code — a tracking-keyed upsert would have erased the lost record
🔑 Rows are keyed strictly by Flexport shipment id, never tracking. Overdue is measured against the resend's ETA, not the already-failed original promise.
✅ The loss and the fix both survive
The order sheet + the ledger
hermes.appolis.app — Order #278526 · Delivery
📦 Replacement shipped. 2 SKUs reported lost · replacement shipped 03-14 · new ETA 03-21 vs original promise 03-04
Replacements ledger
shp_8841originalLOST
shp_9127replaces shp_8841RE-SENT IN PLACE
Detection runs at both Flexport touchpoints — order-sheet open and the delivery cron. The shipping_unremedied crisis signal counts an in-place resend as “made right”.
👁 v0.94 → v0.97.3

Hermes got eyes — then the field tested them

Claude reads the store's images and drafts real alt text from what is actually IN the picture. The guardrails shipped first; the three fixes that hardened them came from real Shopify data.

The pipeline, gate by gate
🎛 Model picker — Opus 4.8 (default) · Fable 5 · Sonnet 5 · Haiku 4.5, sticky per browser. A typo'd id gets a clear 400, never a silent fallback to a different bill.
🔍 Magic-byte format sniff — a PNG wearing a .webp name can't throw a 400 any more.
📏 125-char length gate, three layers — the prompt orders summarising over enumerating, a cheap TEXT-ONLY rewrite pass catches overruns, then a word-boundary trim.
🚫 No-chat guard — one firm retry, then a clean skip. Commentary can never become a draft.
v0.97.1 · video poster frames
7 applies failed under /cdn/shop/files/preview_images/ — the posters Shopify renders for videos match no File and no product image. A third lookup walks the Files library 250/page, cached per batch.
v0.97.2 · the pinned width
The URL pinned its own width=1500 and the shrinker only ADDED width when missing, so the CDN served the full 5.80MB file over Claude's 4.5MB ceiling. It now overrides any pinned width above 800 — the same file at 800 is 1.83MB — with one step down to 400 before a clean skip. No Claude call is ever wasted.
The IT board
hermes.appolis.app — IT
🧙 IT — the webmaster's board
Technical work, pipelined. Live fixes run right here; everything else is stepped out with links.
⬇ Export status (.xlsx)⬆ Import SEO review (.xlsx)
⚡ LIVE FIXES 🧭 GUIDED 🌐 EXTERNAL
High priority · 6 open 12 open · 45 fixed
fl_img_05.webp — alt text missing👁 claude-opus-4-8in progress ▾
neail_quote_pic_copy_V3.webpwidth 800 · 1.83MBdone ▾
✕ Stop (45 done · 130 left)
Scan All drives a loop — 15-a-pass batches, live progress on the button
🔁 v0.97.3 · the honesty slide

One blip in 1,216

The IT health strip surfaced a D1_ERROR: internal error on Gorgias. The fix was to prove it didn't matter — and then fix it anyway.

Investigated, not patched
1 / 1,216
Gorgias webhooks in 3 days. One failure, at 19:06 on 07-22. Cloudflare D1's transient server-side blip.
Nothing was lost
0
The handler 500s → the vendor retries, and the 30-min cron re-pulls updated tickets anyway. The strip's warning ages out of its 24h window on its own.
Hardened anyway
1 retry
Every webhook handler is an idempotent upsert, so a transient D1 error now gets ONE immediate in-request retry — with jittered backoff since v2.52.0, so a herd of retries no longer marches back in lockstep. Real errors are never retried.

The interesting number here isn't the failure rate. It's that a one-in-a-thousand line in a status strip was worth proving out in full before a single line changed.

The sequel, and it is the honest half. A year later it stopped being one in a thousand: v2.52.0 found 424 Shopify and 19 Recharge deliveries failing in 24 hours, every one of them D1 DB is overloaded, climbing 0.5% → 2.4% → 4.5% over three days. The daily checks had watched feed freshness, which stayed green throughout because vendors retry. Nothing watched the failure rate. Something does now — and v2.53.0 went and found what was actually loading the database.

🚨 v0.99.2 · the turning point

Your activity feed was showing strangers

Caught by comparing one customer's Hermes activity to his own Rivo screenshot. The numbers didn't match, and the reason changed the architecture.

Kevin, in Hermes
40 rows
an activity list nobody could reconcile
Kevin, in Rivo
2 rows
+2,500 (Shopify flow) · +2,499 (order) = his exact 4,999 balance
The cause, proven live
GET /points_events is a GLOBAL feed and every customer filter on it is silently ignored — byte-identical rows came back for a real member id, no id, and a bogus id. Five probes, all ignored: filter[customer_id] · q[customer_id_eq] · customer_id · email · ?include=points_events. The tell: page 1 opens with jeff@rivo.io's TikTok follow.
The fix
🪞 Mirror the whole feed into a new rivo_events table
↩️ The cron walks it BACKWARD from the newest page, so recent activity lands first
🔑 Deterministic ids = idempotent; each profile is served from D1 by member id — correct, private, instant
The honest empty state that shipped with it
hermes.appolis.app — Customer profile · Loyalty
Account activity
Syncing loyalty history from Rivo…
Until the mirror reaches a member the UI says this — rather than borrowing someone else's data.
The same vendor trap was already documented on Rivo's /customers list. Now it's documented on this endpoint too.
🎁 The flagship recreation

The loyalty panel

Balance, tier, adjustments, redemption, coupons and the full account activity — one panel on every profile view: full profile, at-risk card, and the ticket side rail.

What the panel promises
It always renders
For a loyalty member the section never hides — v1.2.1's lesson: a hidden section reads as a missing feature rather than an answer. Empty is stated out loud: “No active coupons — redeem their points above to create one.”
The ledger line — the whole system-of-record story in one row of UI
Drift 0 → ⚖️ verified against the Hermes ledger. Drift ≠ 0 → a quiet amber chip: ⚖️ ledger reads N pts (±drift vs Rivo — still settling). Never a silent disagreement.
It stays current without stealing the agent's place
Re-polls every 20s while on screen, refreshes on tab focus, pauses when hidden — and never yanks a date-filtered or paged-deeper view out from under an agent.
The real LoyaltyPanel
hermes.appolis.app — Customer profile · Kevin
🎁 Rivo loyalty FLIP Platinum
4,999 pts · $49.99 redeemable lifetime
⚖️ verified against the Hermes ledger
+ Grant points− Deduct
🎁 Redeem their points for a coupon
Coupons 3 active ↻ Verify
REW-B99D4ADDE7CF$25 off · 2500 ptsON Aug 12
✕ Off charge→ To charge↩ Refund
REW-F6D4464B03E7$10 off · 1000 pts · used
REW-F25578D7BC04$25 off · 2500 pts · refunded
Account activity 118 total 2026-07-012026-07-24 Find
Bonus points — thank you for reaching $300 in lifetime spend! #278526 Jul 24 +2,500
Shopify Flow Add Points: Lifetime spend bonus ($300 tier). Customer lifetime spend: 527.59
Points earned · Sign Up to SMS REVOKED Jul 22 +250
View more (93 older)
The single most important recreation in this deck — every chip, badge and button is the shipped one
🎁 v1.0.0

The redeem button became real

The old “Offer $25.00 in points — zero-margin save” button redeemed nothing. It wrote a toast and a note, and the code behind it would have 405'd anyway.

Before → after, one button
Before
redeemReward() was dead code — it POSTed to a catch-all alias that allows only GET/PUT, and read a discount_code field that doesn't exist.
After — the contract, discovered live without spending anyone's points
// three probes, three honest refusals
bogus customer → CustomerNotFound
real customer + impossible reward → CannotSpendPoints
wrong param → reward_id is missing
POST /points_redemptions {customer_identifier, reward_id}
// coupon returned as `code`
The redeem drawer
hermes.appolis.app — Loyalty · Redeem
Redeem points → coupon
$5 — 500 pts
$10 — 1000 pts
$15 — 1500 pts
$20 — 2000 pts
$25 — 2500 pts — most they can get
Spends their points in Rivo and mints a real coupon in their account. You can copy it for them or put it on a subscription charge.
✓ $25 reward redeemed
REW-B99D4ADDE7CF ⧉ Copy
Or put it on a charge
Aug 12 · FLIP 8$89.00→ Apply
Sep 09 · FLIP 8$89.00already has one
Flat rate: 100 pts = $1 · Recharge allows one discount per charge.

Recreated as it shipped in v1.0.0. What changed since: the code is no longer Rivo's. Since v2.29.0 the reward discount is minted by Hermes in Shopify, and since v2.39.0 each one is its own discount, locked to the customer who paid for it and usable exactly once. Only the points deduction still goes through the vendor.

🎟 v1.2.0 → v2.41.0

Shopify is the truth on a spent coupon

Rivo reports used_at: null even for codes that were spent, so the store has to be asked. It took two goes to ask the right question — the usage counter answers only half of them.

shopifyDiscountStatus(code) — the ladder, as it stands today
usage>0the store already counted a useSPENT
usage 0 → ask for the orderan order carrying the code proves the spend, web checkout or subscription alikeSPENT, DATED
usage 0 + no ordersafe to offer, safe to applySTILL GOOD
404deleted or voided — still asks for the order, because a spent-then-deleted code must still refuse a refundGONE
unresolvableleft honest rather than guessed — a network wobble is never read as evidence of a spendUNKNOWN
v2.41.0 — the counter was blind to every subscription order
A daily check reported 50 reward codes minted, 0 ever used, which read as “nobody can use their codes”. It was the opposite. Shopify's usage count only moves for a normal web checkout — when Recharge bills a subscription the discount comes off the order and the counter stays at zero. Proven on four live paid orders with the money visibly taken off, all four still reading 0.
Why that was a money bug, not a dashboard bug
Four guards read that counter. The storefront return, the agent refund guard, the abandoned-checkout sweep — which actively credits points back — and the nightly walk. Each one would have handed the customer the discount and their points. Fixing the single liveness test fixed all four at once, and the spend is now stamped with the order's own date, so the registry can say “spent on order #292217”.
Wired in three places
The coupon sync verifies 40 per pass (daily re-check, oldest first) · the coupons list drops codes the store no longer has and greys out spent ones · the refund route asks Shopify live before crediting a single point — that one call is the difference between a correct refund and paying twice.
The whole-book pass at v1.2.1 read 585 active · 200 already used · 86 gone · 0 unknown, up from only 55 of 871 ever checked. Two forensic footnotes from that day: all 55 of Tyler's own coupons were gone from Shopify — exactly the “showing up in Hermes but not on the actual site” he reported; and a sweep of all 788 REW- price rules found 0 other orphans, so the single orphan was created by the very bug that release fixed.
The list, in three states · and the refusal rewrite
hermes.appolis.app — Loyalty · Coupons
REW-B99D4ADDE7CF$25 · 2500 pts
✕ Off charge→ To charge↩ Refund
REW-A31F0C8821B7$10 · 1000 pts · used
REW-F25578D7BC04$25 · 2500 pts · refunded
Before — a guess an agent can't act on
“Refund failed: REW-B99D4ADDE7CF no longer exists in Shopify… likely already used or deleted”
After — read out of our own ledger
“already returned for points on Jul 24, 6:20 PM — 1000 pts are back on their balance”
The proof was already in our mirrored rivo_events: “rivo-sub-apply return of REW-F25578D7BC04”, 2500 pts, 22:20:20.
Deliberately keyed on the LEDGER, not a vendor call — any loyalty system we swap Rivo for has an events feed, so this survives the swap.
🔧 v1.2.2 · why the tests didn't catch it

The silent failure

Both loyalty mirrors had stopped storing entirely — without a single visible error — for two independent reasons.

The local test suite
all green
node:sqlite has no bound-parameter cap. Every test passed, every time.
The production mirror
storing nothing
The statement threw on D1, the caller's .catch() swallowed it, and the sync quietly stored nothing.
① The param capD1 caps bound parameters at ~100 per statement. The batched inserts were 30 rows × 12–14 columns = 360–420 parameters.Batch size is now derived from the column count (6–7 rows), never a flat number.
② Cron starvationThe Rivo syncs sat at the END of the 30-minute run — the exact starvation the report-warm comment at the top of scheduled() had documented since 2026-07-11.Their own cron trigger (10,40), their own budget, each sync in its own try, and background refresh failures logged instead of swallowed.
ProofThe events cursor had not moved since it stalled.The :40 run stored all 875 coupons and 3,580 events, and the cursor moved for the first time.

A test suite that passes on a different database engine than production is a test suite that can pass while the product is dead. This is the slide that earns the rest of them.

🏛 v1.3.0 → v2.39.2

Six phases to owning the ledger

Each phase shipped with its own tests, its own live reading, and its own way of being wrong out loud. Twice the reading itself turned out to be the thing that was wrong, and that got shipped too.

Phase 1 · v1.3.0
the seam + the ledger
server/loyalty.js — a provider interface (summary/award/redeem/rewards/earningRules). ledgerBalance() recomputes the balance from mirrored events instead of trusting a column. Continuous reconcile into an IT tile.
7,243 linked members · 98.47% exact.
+4 tests → 152 green
Phase 2 · v1.3.1
seconds, not the next cron
Push ping (/internal/loyalty-touched, 1.2s leash) → on-view tail pull → a */5 * * * * heartbeat lane → a 20s live panel.
+2 tests → 154 green
Phase 3 · v1.4.0 → v1.4.3
our catalog — and the formula was wrong
Hermes mints and owns its own Shopify coupons, and gains the un-redeem Rivo never had. Then 25,000 real balances were pulled from the vendor to certify the arithmetic, and it failed: a plain SUM of every event matched 1,045 of 1,076 members and beat the shipped formula. Rivo writes a clawback as its own offsetting row, so excluding revoked events had been double-counting every one.
→ 161 green
Phase 4 · v1.5.0 → v1.8.0
the earning engine, certified
The program written down as code: tier rate × discounted subtotal. Tested against 23,076 real awards — 99.07% land exactly on a published rate. Knowing who was which tier when was the hard half; capturing the subscription timeline moved the replay 85.30% → 90.82%. Nothing writes points until a forward shadow score clears 99% over 200+ live orders.
→ 165 green
Phase 5 · v2.0.0 → v2.2.0
the switch, and the gate goes green
The flip becomes a real switch: admin, typed confirm, refused while the gate is red. Behind it, a native earn writer that refuses to exist before the flip and a make-good ledger for what the vendor under-paid. On 2026-07-31 the gate returned ready: true, zero blockers — shadow agreement 99.62% over 2,644 orders.
→ 176 green
Phase 6 · v2.29.0 → v2.39.2
Hermes mints every code
Tyler: “cut Rivo out of the equation with the codes.” Reward codes now come from Hermes. Since v2.39.0 each one is its own discount, bound to the customer who paid, usable exactly onceevery live code owned, zero pooled. Points stay the vendor's until the flip.
→ 265 green
The ⚖️ loyalty ledger tile, as it renders on the IT board
hermes.appolis.app — IT · health
⚖️ loyalty ledger100% EXACT
59,916 members · 0 unexplained · checked 12m ago
🛒 ShopifyHEALTHY
sync 4m ago · hooks 24h: 312✓ 0✕
🎁 RivoWATCH
sync 4m ago · hooks 24h: 312✓ 1✕
Dot thresholds: ≥99% mint · ≥97% gold · below that, coral.
Every phase found a measurement trap before it found a bug. Scoring against ALL customers produced 15,091 phantom drifts, because a zero in the vendor column is the ingest default and not a synced value — scope corrected to members with a real link. Then the excuse window meant to forgive mid-flight members was seven days wide, which forgave everybody and pinned the tile at a fake 100%. It took until v2.54.0 to find the arithmetic that settles the question honestly.
🏛 v1.4.0 → v2.39.2 · the flagship

The un-redeem the vendor never had

Hermes mints its own Shopify coupons, keeps its own book, and can take one back. The whole ghost-coupon class exists because a vendor mints codes we can't see and can never un-redeem — this is the piece that ends it.

Order matters — deliberately
mint → deductmint fails → nothing happened. Safe.
mint → deductdeduct fails → coupon deleted, redemption row removed. Recoverable.
deduct → mintmint fails → the customer is short with nothing, and it cannot be undone — the vendor has no un-redeem. That is the exact reason this whole phase exists.
One discount per redemption — the shape as it mints today
// v2.39.0 — DiscountCodeBasic, one per redemption
customerSelection.customers.add: [ the buyer ]
usageLimit: 1 // safe: this discount hosts one code
appliesOncePerCustomer: true
combinesWith: shipping only // two rewards never stack
title: "FLIP Reward $25.00 — REW-…"
The detour that had to be undone — v2.39.0
To stop the vendor flooding the admin with one discount per redemption, codes were briefly pooled: one standing discount per reward value, many codes inside. The minter's own comment asserted that the usage limit applied per code. It does not — Shopify's schema is explicit that it applies to the discount, and a redeem code has no limit field at all. The $25 pool held eight codes sharing a single use. The first customer to check out would have consumed the pool and the other seven would have failed at checkout after their owners had already spent the points. Nothing had fired yet: all four pools still read zero. And no pooled code could be tied to anybody, so any code worked for anyone holding it. Both faults died with the same change.
The original mint shape was copied from a live vendor code in the store, not guessed — a near-miss behaves differently at checkout. On a failed create the discount is rolled back rather than left orphaned in the admin. And POST /api/loyalty/void-coupon deletes the Shopify object and credits the points, claim-before-credit so a lost response can't pay twice.
Our catalog — loyalty_rewards
hermes.appolis.app — IT · loyalty_rewards
Rewards catalog · seeded FROM Rivo
$5 off500 ptsvalue_cents 500ACTIVE
$10 off1000 pts1000ACTIVE
$15 off1500 pts1500ACTIVE
$20 off2000 pts2000ACTIVE
$25 off2500 pts2500ACTIVE
$50 off (vendor dropped it)5000 pts5000DISABLED
Values stored in CENTS so money never rides a float. Import is idempotent on (tenant, vendor, vendor_reward_id) — a reward the vendor drops is disabled, never deleted, because a redemption in our book may still point at it. Live: all 5 rewards imported.
✅ Live since v2.29.0 — native_redeem is on for this tenant
Turning it on found four bugs the same day, all of them latent and all of them fatal the moment the switch moved: the mint could never match a reward because it compared our id against the vendor's; every pooled mint produced the same ledger row id, so only the first could ever succeed; a minted code was written to a table no screen reads, so it existed in Shopify and appeared nowhere; and the nightly sweep, which deletes anything the vendor's feed does not return, would have deleted every Hermes code by definition. A test now mints two codes, runs a full sweep against an empty vendor feed, and asserts both survive.
🚦 The readiness gate · shipped 2026-07-24, green 2026-07-31

A gate that costs the vendor nothing

GET /api/loyalty/readiness — admin, zero vendor calls, so asking never costs Rivo anything. It shipped saying NO, and it meant it. Six days later it said yes, and it meant that too.

first reading, 2026-07-24 — blockers[]
  • Events mirror still backfilling — page 214 of 468
  • 25,380 events stored · ~4h left at 25 pages per half-hour
  • Ledger balances are therefore genuinely partial
still_vendor_owned[]
  • The points balance
  • Earning rules
  • VIP tiers
Named in muted honesty: what a flip does NOT remove. The storefront came off this list — v2.28.0 and v2.38.0 moved every reward surface a customer can see onto Hermes.
READY

2026-07-31 — ready: true, zero blockers. Shadow agreement 99.62% over 2,644 live orders; every one of the five surviving ledger disagreements confirmed exact against the vendor's own API and healed on the spot.

The readiness payload, rendered
hermes.appolis.app — IT · loyalty readiness
Native redeem switchON
Native earn switchOFF — until the flip
Catalog5 rewards imported
Our minted couponsevery live code owned · zero pooled
Ledger accuracy100% · 59,916 members · 0 unexplained
Forward shadow score
99.62% agreement over 2,644 live orders · gate wants ≥99% over 200+
blockers
none

Green is not the same as thrown. The flip is still one deliberate action — admin, typed confirm: "FLIP", refused twice — and it has not been taken. What it does on the day changed as recently as v2.53.1: the make-good rows recording where the vendor under-paid subscribers, 106 of them worth 28,871 points, are now waived rather than credited, on Tyler's call — “nobody needs to get awarded points that they do not know about.” The rows survive as the only record of the shortfall, because that settlement is being handled with the vendor directly. Without that change, flip day would have paid all 106 out in one unattended pass that refuses to run twice.

⚖️ v2.52.0 → v2.54.0 · the one that matters most

Two hundreds. Only one of them was real.

The loyalty tile read a perfect score for weeks. It was lying, and the way it was lying is the most useful thing in this deck.

v2.52.0 — the fake one
100%
The real figure was 93.4%, with 1,069 members drifting. The window meant to forgive a member caught mid-sync was seven days wide, and the sync touches those timestamps constantly — so every drifter qualified for the excuse, was subtracted from the denominator, and the tile divided a number by itself. Forever green. The cutover blocker watching that number could never trip.
v2.54.0 — the real one
100%
59,916 linked members compared across the whole book on 2026-08-17. Eleven looked wrong. All eleven were proven to be measurement lag — the vendor's reading is a snapshot, and points keep moving after it was taken. Zero genuine mismatches.
The test is arithmetic, not a hunch
// prove the two sides agreed at the moment of the reading
vendor column
+ Σ( our events after that reading's timestamp )
== our ledger → explained: lag, not error
// and it must hold in both directions
ledger > column → they earned after the reading
ledger < column → they spent after the reading
Before this shipped, the daily report named “drifting” customers twice in one day and two sessions went chasing people who were completely fine. Verified after shipping: the query now names zero members on the live book.
Why this is not the old bug wearing a new coat
What it accepts“Some event is newer than the reading” — nearly always trueThe difference has to match to the point
What it does with the rowRemoves it from the denominator — the member vanishes from the scoreCounts it in the numerator and leaves it in the denominator
The blockerCould never tripUnverified drift is its own reported number, with its own ceiling

Strictly more conservative than the rule it sits beside — even a vendor-confirmed pending is allowed to leave the score, and this one is not.

A number that was earlier reported as 99.99% was itself an artefact of the same thing: comparing a ledger that updates continuously against a vendor column that refreshes on its own cadence. The honest answer was better than the reported one, which is why it was worth proving rather than accepting.

🛒 v2.1.0 → v2.38.0

The storefront comes home

Tyler: “couldn't Hermes just handle that stuff internally?” It can, and now it does — every reward surface a customer can touch is served by the same brain the agents use.

Four moves, in order
🛍 Inside Shopify admin (v2.1.0). A real page in the merchant's own admin, authenticated with Shopify's session tokens and zero new secrets — the token itself names which tenant's credentials verify it, so a second merchant works without a second build.
🔌 The account blocks (v2.3.0). The customer-account extensions moved onto Hermes and were re-doored onto organs it already owned. The audit found 143 members the loyalty program called subscribers with no subscription record at all; healing them proved most were the same human under two email addresses. 143 → 5.
🤝 Referrals, native (v2.4.0). Full parity with the live program — 1,500 points to the advocate, $15 off for the friend — but attribution rides the friend's own minted code instead of a cookie, so it survives a change of device or browser.
🏛 The rewards page and the launcher (v2.5.0). A branded rewards page and a floating drawer, replacing the vendor's hosted page. The app proxy was repointed to Hermes in the same release.
And the week nobody noticed. The widget calls nine routes. Only five came across with the proxy. The other four returned this door's 404 — and a 404 body is still JSON, so the widget never raised an error. It read “no charges, no state” and told customers “No upcoming subscription orders found on your account.” One gap, every storefront symptom. A test now asserts that no widget route 404s.
v2.38.0 — Hermes draws the list itself
flipmylifenow.com — Rewards · my rewards
Your rewards
$25 off Spent 2,500 Points
$10 off Spent 1,000 Points
The card is a lid, not a drawer: name and points only, never the code. Putting a code in the card text recruits the whole button stack onto every row — the widget's own decorator keys off a visible code.
The unexpectedly good part
The vendor's own modal will happily render a Hermes code — right title, code in the box, apply-to-cart, all four actions. Verified live on a code the vendor has no knowledge of. So no lookalike modal was ever built; Hermes opens theirs.
🪞 Mirror → shadow → flip, the house pattern

It shipped in shadow first: fetching, rendering nothing, and reporting what it would have replaced. That side-by-side is what decided the swap. And it fails open by construction — the vendor's own block is hidden only after ours has actually mounted, so a failed request means our decoration doesn't apply, never an empty rewards page.

🎟 v2.40.0 → v2.51.0

Every code in the store, and whose it is

Shopify's discount list is a flat pile that cannot answer “whose is this?”. The first attempt at an answer was another list, and Tyler said so: “you made me exactly what I did not want.” Four layout options later, this is the shape he picked.

36,397
uses on a single automatic discount — a whole class of machinery the board could not see at all until v2.47.0
~1,600
reward codes in the store, which drowned the in-house campaigns out of a newest-first list until the feed was filtered at Shopify
1,278
verified-group redemptions — Military 1,031, First Responder 247 — now their own section, because a benefit is not a campaign
50
human verdicts stored as data, so classifying a code never needs a deploy again
hermes.appolis.app — Discounts · the deck
LoyaltyBothIn-house one payload, filtered in the browser — flipping the switch is instant
The money strip
Live value outRedeemedPoints liabilityReturned
Loyalty money is knowable to the cent, because 100 points is $1. A percentage-off code's value is not — so the store side counts rather than pretends.
🔄 Lifecycle flow
Active → Spent → Returned → Gone, as money totals. An active code idle over 14 days is flagged ⚠ nudge? — a customer who paid points for nothing yet.
👛 Wallet search
Type a name, an email or a code. The answer is a person you can act on: give or take points, redeem on their behalf, return a code, full profile one tap away.
🗳 Rulings
A verdict on a code beats every built-in pattern. Rule a family once and it covers whatever that machine mints tomorrow — proven on two codes that did not exist yet.
Admin-only, and read-only where it counts — a browse surface that cannot mutate cannot break a live store

The deck exists because of a decision made two releases earlier: used codes stay in Shopify rather than being deleted. Deleting them keeps the admin tidy and destroys the only way to ask the store “was this spent?” — the check that exists because a private list of dead codes once caused a real double refund. So the store's list grows on purpose, and this is where anyone actually looks.

🎁 v2.45.0 → v2.49.0

Give a thousand people points without giving anyone them twice

“Everyone who spent $X in the last N days gets Y points.” It moves real money in the balance customers can see, so it was built adversarially from the first line.

The four rules it will not bend
preview firstThe award button does not exist until the group has actually been looked at. A live count re-runs on every change to the filter.
drift refusalApplying re-counts the group. If it changed since the preview, it refuses rather than paying a different set of people than the one that was approved — orders land constantly.
receipt firstA deterministic receipt per member, per campaign, per day is written and checked before the vendor is called. A crash and a retry pays nobody twice — and a failed award holds no receipt, so the retry pays exactly the ones that were missed.
the key hashes the filterTwo different campaigns on the same day both pay. A re-run of the same campaign pays nobody. One test runs both cases against the same members.
The drift guard caught the test suite itself asserting a hardcoded count the seed data disagreed with — which is precisely its job. Awards go out in batches of 100 with an explicit remainder, and the filters compile to indexed per-candidate probes rather than one wide join, because the wide join is what took the database down two releases earlier.
The Composer
hermes.appolis.app — Discounts · Bulk
Who
spent ≥ $150 in 90 days active subscriber holds an idle reward code cancelled a sub in 30 days points balance ≥ 1,000
1,284 customers match · 500 points each · running cost $6,420
Plays
🐋 Whalesopen
💎 Subscriber thank-youopen
🔁 Win back the cancelledopen
😴 Nudge the idle codesopen
Every play is priced live when it opens — the real match count and the real dollar cost, not a saved guess. Saving your own setup puts it on the same shelf.
Recreated with sample figures — the chips and plays are the shipped ones
📬 v2.52.0 → v2.53.0

“It said sent.” It was never sent.

Tyler replied to a real customer from his phone. The app said success. Nothing arrived. It had nothing to do with the phone.

Three faults, one message
🚫 The reply was structurally malformed. A required field was never sent, so a reply could fail in an agent's face with a raw vendor error — which is exactly what an employee saw.
🤥 Accepted is not delivered. The vendor accepts the message and sends it asynchronously. The app toasted “Reply sent” on any success code and never looked again.
📮 And it was sent from the wrong address. The vendor only delivers email from its own registered sending identity. A reply “from” the API login account is accepted and then quietly binned.
🔍 The send now checks. It probes the message a second and a half later and reports sent · failed · pending. The very first live send after this shipped was accepted and then silently failed — and the probe caught it. Under the old code that would have read “Reply sent.”
📌 The sender is pinned. Every outbound email leaves from the business support address, through one choke point that refuses loudly if the pin is missing.
The toast tells the truth now
hermes.appolis.app — Conversation · reply
✓ Reply delivered — from support@flipmylifenow.com
✕ Accepted, then failed to send — the vendor took it and could not deliver it
⏳ Still pending — checked once, no verdict yet. Said out loud rather than guessed.
Chat and social still mirror the customer's own address, and must — on those platforms the address is a platform handle, and more than a third of this tenant's tickets arrive that way. A missing sender is now a loud error, because an empty one serialises into something the vendor accepts and never delivers.

The uncomfortable footnote, kept rather than buried: return-label emails rode the same broken sender, so some labels sent before this may never have arrived.

📣 The big idea

Marketing should come
from customer service

Who knows what customers want better than the people they're already talking to? In HERMES the two systems are one — so the voice of the customer IS the marketing brief.

1
51,262 real conversations mined into ranked pain themes — cancel reasons, shipping pain, taste, price — in the customers' own words
2
Every save attempt is marketing tested at the moment of churn — we know which offers actually keep people, confirmed by paid reorders
3
Win-back lists build themselves — cancelled subscribers and quiet one-timers, segmented and ready for a campaign
4
One-click reports turn support reality into campaign strategy — no survey, no agency, no guessing
hermes.appolis.app — Insights · Top pain points
Top pain points
Cancel requests
Refunds
Shipping & delivery
Product taste
Skips & pauses
Address changes
Ranked from 51,262 real conversations — this list IS the marketing brief

These four numbers used to sit at the top of the Support Hub, above the work. v2.24.0 took them out: they are figures you read at a desk, not the thing an agent needs first thing in the morning. They live in the Insights report builder now, where they run over the whole book rather than one rotating slice — and taking them out also killed a cron job that had been scanning 400 ticket bodies a tick to warm a cache nothing read.

🃏 HERMES in eight answers

Remember this

QHow fast does any queue category open?
A~200ms, from the precomputed cache — at 150k+ customers. The rebuild belongs to the cron; a request never does it.
QWhat counts as a "save"?
AOnly a paid reorder. Free product never counts.
QWhen does a return get refunded?
AWhen the warehouse physically has it — a task fires automatically.
QWho gets flagged as a heat case?
ACustomers who are angry in their OWN words — not keyword noise.
QWhere do marketing insights come from?
ACustomer service. The two systems are one.
QWho keeps the loyalty ledger now?
AHermes does — a plain sum over every mirrored event, clawbacks included, because the vendor writes a clawback as its own offsetting row.
QWho mints a reward code?
AHermes. One Shopify discount per redemption, locked to the buyer, usable once. Every live code is owned; none are pooled.
QHas the loyalty flip happened?
ANo. The gate has been green since 2026-07-31. Throwing it is still one deliberate, typed-confirm action, and it is Tyler's.

One team. One screen. One ledger. 1,252 tests green · 0 genuine ledger mismatches across 59,916 members.

⬢ Feature Overhaul · v1.4.2 → v2.54.0 · 2026-07-29 → 2026-08-17

Rivo's job kept getting smaller.

Tyler: “Hermes is essentially going to be the system of record for everything eventually anyways.” Nobody ripped the vendor out. One hundred eight releases just kept taking pieces of the job and proving each one worked before taking the next.

the seam
server/loyalty.js — a provider interface. Swapping vendors is “write a second provider, zero route changes”
100%
ledger correct on the whole book — 59,916 members compared, 11 apparent gaps, all 11 proven to be measurement lag, 0 genuine
zero pooled
every live reward code is its own Shopify discount, bound to the customer who paid and usable exactly once
9 routes
the whole storefront widget served by Hermes — and since v2.38.0 Hermes draws the rewards list a customer sees, which the vendor could never do for a code it did not mint
1,500 / $15
the referral engine, rebuilt native at full parity with the live program — attribution rides the friend's own minted code, so it survives devices and browsers
161 → 323
tests green across the arc — the suite doubled, and several of its members exist because a bug got asserted as a guarantee first
The balanceWhatever the vendor's column said. No independent recompute.Recomputed from mirrored events — and the formula itself was corrected by certification, not by opinion
Reward codesThe vendor minted codes Hermes couldn't see and could never un-redeemHermes mints them: one discount per redemption, owned, single-use, voidable
What the customer seesThe vendor's hosted rewards page. A Hermes-minted code appeared nowhereHermes draws the rewards block, Ways to Redeem and the redeemed list — plus its own rewards page and launcher
“Was this spent?”Shopify's usage counter — which never moves for a subscription order, so four money guards were blindAsk for the order that carries the code. True for web checkout and subscription billing alike, and it names the order
The store's discountsA flat pile of codes Shopify cannot attribute to anyoneA registry that knows whose each one is — plus 50 human rulings stored as data, so a verdict never needs a deploy

What is still the vendor's: the points balance, the earning rules and the tier ladder. The engine that replaces them is built, certified and switched off — waiting on one typed confirmation.

Feature overhaul · replaced only by a newer overhaul of the same type
⬢ Performance Overhaul · v1.4.2 → v2.54.0

Four full table scans, found and cut

Every one of these looked like something else first — a slow app, a vendor outage, a database problem. Every one was a query reading the entire customer table to answer a question about one person. All four were measured against production before a line changed.

154,659 → 0
Every customer lookup (v1.4.2) — the code matched on lowercased email, the index was on the plain column, and an index cannot serve an expression. One Recharge sync walked ~500 records and scanned ~77 million rows. The connectors had been silently dead for two days
reset → 3.2ms
The discount deck (v2.45.2) — one OR inside a join condition, which SQLite cannot optimise, so it scanned the 138k customer table per coupon row. Run alone against production it returned “D1 exceeded its CPU time limit and was reset”. Every queue error that morning was collateral from a click on this tab
157,468 → 21
The webhook hot path (v2.53.0) — the duplicate-healing lookup OR'd email against alternate emails in one query, so every single order, customer and subscription delivery read the whole tenant to serve the 19 customers who have a second address. 132.7ms → 1.4ms per delivery
718,562 rows
The at-risk queue (v2.54.0) — a cache miss used to rebuild inside the request; the first query alone reads 718,562 rows in 618ms before grouping every order, shipment, subscription and ticket. The page agents hit hardest was the one that could take the worker down
one wave a day
The nightly burst, correctly named (v2.53.0) — read as “a wave every hour”, it is the subscription billing run just after midnight, ~250 charges fanning into a webhook storm. Retry backoff preserves a delivery's original minute, which painted the same signature into later hours. Stripping redeliveries: 4,167 first-ever deliveries in that hour against 204 in the one before
503, not 500
Where the invisible failures were (v2.53.0) — the settings read sat outside the webhook handler's own error net, so an overloaded database escaped as a bare 500 with no health-log row of any status. Deliveries vanished from the log entirely. Both that and the session read now answer a retryable 503

Baseline underneath all of it: the precomputed queue_cache serves every at-risk category and the VIP list in ~200ms at 150k+ customers. Since v2.54.0 the cron owns the rebuild — a miss serves the last good slice stamped with its age, and a genuinely empty cache says “building” rather than dressing an empty list up as “nobody at risk”.

The standing lesson, now written on the routes themselves: never OR two indexed columns in one condition. The per-row scan is invisible locally, because seed data is tiny, and lethal at production scale. Timing the exact query against the real database is a two-minute check that catches all four of these.

Performance overhaul · replaced only by a newer overhaul of the same type
📜 Changelog · 2026-07-24 → 2026-09-10 · v2.78.0–v2.147.1 and through v2.54.0 · v2.55–v2.77 not yet folded in

126 releases since v2.54.0, when
this deck last looked.

v2.147.1
Two words fixed in the new scheduler. The confirm button and the "call booked" message now say the time the way the rest of Hermes does (09-10-26 4:42 PM) instead of a raw computer timestamp, and task rows and reminders name the topic in plain words ("Shipping issue", "Billing & account") rather than the internal shorthand.
v2.147.0
Book a call the way you would book anything else. Scheduling a call or text now opens a real date-and-time picker instead of five canned slots, and "let them pick" mints a link that actually works: the customer opens a clean Flip My Life page, chooses a time in their own time zone, and it lands on the agent's task list. Every booked follow-up now belongs to the person who booked it (or a teammate they choose) and rings them before it is due: a banner at the top of Hermes, a short chime, and a desktop alert when the browser allows it. Text is only offered to customers with a mobile number who said yes to texts.
v2.146.0
The inbox door opens for everyone who has one. Sending a note to a customer's FLIP HQ inbox from the support hub used to fail with "no store account" even when they had one, because the profile never passed the account along. It does now, the full profile gets the same inbox button, and every action on a profile only appears when it is actually possible for that customer — no more offering a subscription credit to someone with no subscription.
v2.145.0
The rewards page tells a subscriber the truth. An active subscriber is FLIP Fam, full stop — but the store's rewards page was still reading an old copy of the tier and telling 898 subscribers they were "FLIP Member" with a "spend more" nudge. It now works out the tier live, the same way the account hub does: an active subscription (or an upcoming order already on file) means FLIP Fam; everyone else sits where their lifetime spend puts them. One rule, every screen. Points and earning are untouched.
v2.144.0
A long email address can no longer break a customer update. The database Hermes runs on allows a search pattern of only 50 characters, and the check that matches a customer by an alternate address wrapped the whole address into one — so any address of 47 or more characters made that update fail, and the store kept retrying until it gave up. That check now uses a plain "contains" lookup with no limit, every search box trims what it sends to the budget, and the test suite enforces the same 50-character rule, so this cannot come back unseen. The new rule immediately caught a second case nobody had reported: the site chat's confirm step, which the same limit would have broken on its first real use. Fixed before it happened.
v2.143.0
A gift is not a refund. When a creator who is also a customer is sent free product, that send no longer counts against their profitability as a customer, and it no longer counts as a "problem solved" replacement for a shipping issue. The two things were always separate; now the numbers say so too.
v2.142.0
Find anyone, see only what a recruiter needs. The affiliate manager's desk gains a Customer search between the VIP list and New messages: type a name or an email and it looks through every customer the store has ever had, not just the top spenders, and shows each one as the same compact card with the same buttons — profile, message, invite, note, call. The full customer-service profile stays out of reach. The little chip strips on that desk no longer show a scrollbar.
v2.141.0
One card per order. A member with bags on different dates now sees each upcoming order as its own card — its date, its bags, its total — instead of every bag piled under the first date; each bag can change its own schedule or all of them together; a reward comes off the bags in the order it applies to and no others; and swapping a flavor keeps the price that member already pays. Found by a support agent walking through her own account, fixed on both sides of the portal in one round.
v2.140.0
One place for a person. From a creator's record the affiliate manager can now open their affiliate profile or message them straight away — a creator who was never a customer gets a record made for them on that press — and from the profile he can add a creator record. The creators list offers the profile and the conversation on every row. Logging a note now asks what happened and keeps what he types, and a separate Log a call button records a call.
v2.139.0
One way to reach a creator, and one desk to do it from. Every message the affiliate manager sends now lands in the creator's FLIP HQ inbox and goes out again by email with a button that brings them back to that inbox to answer, plus a line asking them not to reply by mail. Invitations use the same path. The desk is three sections — a Dashboard with the creators list and the message templates built in, the VIP list, and New messages — the conversation itself offers the templates or a blank page, and the chip strips give a phone a little more room.
v2.138.0
One profile on the affiliates desk, and it is the affiliate one. A View profile button on every VIP card, creator row and conversation opens a pop-up built for the affiliate manager: what someone is worth, how long they have been with you, what they buy, whether they subscribe, where they stand in the funnel, the conversation so far, and whether they are already one of your creators. The full customer-service profile is no longer reachable from that desk, and a VIP card no longer drops you into the inbox.
v2.137.0
The affiliate manager gets a proper inbox — and so does every creator he talks to. Messages between him and the people he is recruiting now live in one place inside the app, and each creator sees the same conversation in their own FLIP HQ account, in a new Affiliate folder they can answer from. Clicking a creator opens that conversation and the few facts that matter for recruiting, instead of the full customer-service profile that was never his job. Creator profiles gained a drop-down that adds someone to his list in one press. And the invitation emails appear in the thread with a note saying replies to those arrive in his own mailbox — the app never pretends to hold half a conversation it cannot see.
v2.136.1
The recruiting desk lost the customer-service clutter — and three colleagues who never existed. Opening it for real turned up three faults in an hour: a small arrow button that, when pressed, squashed each person's name into a single vertical letter; a "Reach out" button that offered support topics and promised to route them to Dana, Miguel and Sasha, none of whom work here (they came from old demo data); and a daily sending limit nobody asked for. The row now carries only what recruiting needs, the limit is gone, and the booking screen no longer names anyone it cannot vouch for.
v2.136.0
Scott can recruit affiliates from the VIP list, by personal email, inside Hermes. A new My creators section on the affiliates desk: pick any top customer, claimed or not, and send them a personal invitation from Scott — three editable templates that fill in the person's name, how long they've been a customer and their orders — with an opt-out line on every mail, a daily limit, and a record of every invite on the customer. Prospect, invited, replied, affiliate or declined, at a glance.
v2.135.0
The "working" card now says "Done". When a longer action finishes, the centred card no longer just disappears: it turns into a green tick with what happened and how long it took, stays a moment, then goes — so nobody is left wondering whether the thing they waited for actually went through.
v2.134.2
The deck tells the truth about its own version. Its opening and closing slides still said v2.54.0 and 323 tests from mid-August while the rows ran to v2.134.1. They now say the current version and test count, and a check in the test suite fails any future release whose deck still carries the old numbers.
v2.134.1
The Portal tab opens fast. Opening the customer-portal dashboard sometimes meant a two-minute wait while the app rebuilt two days of usage figures one row at a time and re-read every order. The figures are now rebuilt in one stroke and refreshed by the half-hourly background job before anyone asks, so the tab reads a ready-made snapshot.
v2.134.0
Refer-a-friend rewards actually pay out now. A check that was supposed to notice when a referred friend placed their first order had been failing quietly on every run since early August, so no advocate had ever received their referral points. The check now works, says plainly why if it ever fails again, and on its first run pays anyone whose friend already ordered.
v2.133.0
Change one box, or every box — the portal can now tell the difference. Behind the scenes, the account portal gained one door that edits the next order: add, remove, swap a flavor or change a quantity, either for just that box or for every box from now on, with an Undo for the one-box kind. A swap for one box charges the subscriber price, never the retail price. And churn on the dashboard now uses Recharge's own definition, so the two never disagree.
v2.132.0
Survey answers become checked, suggested actions. Each answer a member gives is read against what that member actually did in the portal, so the team sees whether a complaint holds up, and gets two or three concrete things to fix, reword, guide or keep — with a Plan / Done / Dismiss switch and the cost shown before it runs.
v2.131.0
A dashboard for the customer portal, counting from the day it went live. A new Portal tab shows who signs in and where they go, what they do for themselves, which flows they abandon, what fails, what members say, where they ask for help, and the business numbers week by week since activation — with a red banner the moment something starts failing.
v2.130.0
The customer portal gets its own report. Insights gains a Customer Portal tile and a full deck: who signs in and what they do for themselves, whether anything is failing, what members say when asked, and how cancellations, churn, orders and support tickets look before and after the launch — every number defined on the last slide.
v2.129.0
The database now has to prove it was built. Months ago a piece of the setup script was skipped in silence: the app built 47 of its 49 tables, every page still looked right, and the only clue was one real person who could never receive their sign-in code. From now on the app counts what it was told to build against what actually exists and refuses to call a half-finished setup a success, naming exactly what is missing. A release check runs the same comparison after every deploy, so a gap is loud instead of invisible. Checked today against the live database: everything it declares is there.
v2.128.0
We counted every doubled message, then stopped showing them. A sweep of all 194,017 stored messages found just five that the app itself had duplicated, on three conversations, two of them real customers — copies of a customer's own words coming back from the help desk, all written before last night's fix closed that door. Everything else that looked doubled was the help desk's own chat wording, which is theirs, not ours. Rather than reach into the record and delete anything, the app now recognises a copy when it reads one, so no conversation shows the same message twice, anywhere, however old it is.
v2.127.0
Every form message is its own thread in your inbox. Send a question through the site's form and it shows up in your FLIP HQ inbox as its own message, marked "Sent · waiting for a reply", instead of being folded into an older conversation. When support answers, the reply lands under that message. Older threads stay readable, and an "Open the latest" button keeps everyone writing in one place — because on the support side it is still one conversation per customer, so nobody can flood the help desk with duplicate tickets.
v2.126.0
Tell us your birthday, see who claimed your link. Members set a birthday month and day in their account so the birthday reward can pay out, and the referral card shows the email each claimed code went to.
v2.125.0
The first real photo taught two more lessons. Tyler sent a form message with a picture: the picture reached the help desk this time, through the signed link the previous release opened, but Hermes and his own account inbox showed the message twice. Two fixes. The piece of the app that stores uploaded files could write them but had never been taught to read them back, so the help desk was only ever handed a link; it is now handed the file itself through the help desk's own upload service, a copy that outlives the link and does not depend on our door. And the doubling came from the mirror: it already knew to skip the copy of a customer's words that comes back from the help desk, but only for conversations that started on the website — and since yesterday a repeat message joins the customer's open conversation wherever it began. Both are fixed: the file store reads, and the mirror skips the echo on every conversation.
v2.124.0
Your points-per-dollar, not the base rate. The "Place an order" reward now shows what your own tier earns — a FLIP Fam member sees 10 per $1, not 5.
v2.123.0
The inbox keeps up, and carries files. Whichever conversation moved last sits on top; photos and PDFs travel both ways. Members can join the text list from inside FLIP HQ and get their points on the spot.
v2.122.0
Referrals you can act on. Each friend shows where they are — claimed, reminded, ordered — with their code, a text/email button from your own phone, and the exact time the next email reminder opens. Inbox messages can carry a picture.
v2.121.0
The founder's letter waits in the inbox, looking like the letter. A member's inbox now holds the pop-up letters with their photo from the first visit on, unread until the pop-up itself has been seen.
v2.120.0
Write to a member's inbox from Hermes. From any customer's panel, send a letter, an update or an offer straight to their FLIP HQ inbox — and an offer can carry a one-time discount code minted for that person alone, so a leaked code is worthless to anyone else.
v2.119.0
An inbox for every member. Hermes now keeps one list of what the brand has said to a member — support replies straight from their own conversations, the founder's letters, and soon offers and updates — with unread counts, read and archive, and a reply box that lands right back on the help-desk conversation.
v2.118.0
Your reward shows in the price. When a reward sits on the next box, FLIP HQ now shows the original price struck through next to what you actually pay — everywhere that order's price appears — because Hermes sends the discount amount along with the total.
v2.117.0
The photo reaches the help desk too. The release two steps back made every stored file visible in Hermes and handed the help desk a link to it — and the help desk showed nothing. Two reasons: the link was unreachable from outside, because the door that serves it had been opened in the test copy of the app and not in the live one, and the help desk only displays files that were uploaded through its own upload endpoint in the first place. Both are fixed. The live app now opens that door, a test makes sure the live and test copies can never disagree about it again, and the message the help desk receives now carries the actual file bytes, uploaded through its own endpoint, so an agent working there sees the customer's photo. If that upload ever fails, the message still goes, the file rides as the signed link instead, and the failure is written on the ticket where someone will see it.
v2.116.0
Buy something once, with your next box. A subscriber can drop a one-time item into their next delivery from FLIP HQ — same box, same date, charged once at the store price — without touching their subscription lines. Hermes reads the price from the store itself and refuses anything the store can't sell right now.
v2.115.0
Cancel or re-address an order yourself — while the warehouse hasn't started on it. From the order page in FLIP HQ a member can now cancel an open order and get the full refund in the same motion, or change where it ships, under exactly the rule the help desk already follows: once the warehouse has picked it up the buttons say so instead of pretending. Every action is checked against who owns the order and written to the customer's activity log with the member named as the one who did it.
v2.114.0
The photo comes through — in both systems. A customer who attached a picture to the website form had it saved on the first day, and then nobody could see it: Hermes only drew pictures that lived on the help desk's servers, other files were not shown at all, and the help desk received a line of text naming the file. Every stored attachment now appears on the conversation in Hermes — pictures inline, other files as small chips that open in a new tab — through a door that requires the same sign-in as the conversation itself. And the copy of the message that goes to the help desk now carries the actual files, delivered through signed, expiring links that only ever hand out the customer's own uploads, so an agent working there sees the photo too.
v2.113.0
The repair passes learn from their first night. Two runs in, the numbers said two things. The refund repair was doing 94 orders a pass and overrunning its time, because it recomputed the customers only after checking the clock; it now fetches four orders at once and keeps the recomputes inside the budget, so a pass covers roughly four times as many. And the subscription repair had started at the wrong end: the oldest "stale" subscriptions turned out to be genuinely active with a failing card, which the help desk already knew, while the recent ones were the real missed cancellations. It now walks newest first, which is where Tyler's own stray subscription was — corrected on the second pass, and it would have been the first.
v2.112.0
The customer profile tells the truth about money and subscriptions. Tyler opened his own profile and found two active subscriptions where he has one, and cancelled orders still showing a price. Neither was a display bug. A subscription that ended in July was still marked active because nothing ever re-checked a subscription that had simply stopped changing — 554 of them across the store. And most refunded orders had never been told how much was refunded, so lifetime spend, which already knows to subtract refunds, had nothing to subtract — 6,469 orders. Two small repair passes now run every half hour: one re-reads every stale subscription from Recharge, the other fetches every amount-less refund from Shopify and recomputes the customer, and each reports how many remain until it reads zero. On the profile, a refunded order now shows its real net with the original struck through, and an order refunded before it ever shipped reads cancelled, not late.
v2.111.0
The first dashboard paint cannot hang. Tyler: “every now and then the screen hangs when trying to load the initial dashboard — sometimes from a refresh.” The account dashboard's first request waited on four upstream reads with no time limit on any of them, then made one more call to Recharge only after all four had answered — so a single slow vendor held the whole page blank until Shopify's proxy gave up. Every one of those reads now has its own time limit and fails honestly with the dashboard's retry card instead of hanging; the Recharge read runs alongside the others instead of after them; and every request writes one timing line naming where the seconds went, so a slow vendor is identified from the log rather than guessed at.
v2.110.0
Referral codes that actually work — and a place to watch them. Tyler tried his own referral code at checkout and it was refused: every friend code sat inside one shared discount that Shopify would only honour once, and that discount was never allowed on subscriptions. Each friend code is now its own discount — one use, one customer, good on one-time and subscription orders alike — and the invitation page's Shop button carries the code straight into the store, so it is applied before the friend ever sees the cart (a Copy button keeps it for later). The rewards dashboard's referral section can now show every friend who claimed a code, whether they have ordered yet, and a Nudge button that sends them one reminder email with their code and the same one-tap link — never before the claim is a day old, never twice in three days, three times at most.
v2.109.0
The name on the email, and nothing behind the curtain. The first real reply through the new form arrived signed “Support Flipmylifenow”, the mailbox's own display name. The Gorgias connection now has a “Sender name customers see” field, and Hermes puts that name on every email it sends; the same name belongs in the help desk's channel settings and on the mailbox itself, so all three agree. The reply also quoted the customer's original message back, footer and all, and that footer mentioned the system by name and said where files were “stored”. It now reads simply as a list of attachments and a reference number, which is what a customer quoting it actually needs.
v2.108.0
One conversation per customer, however many times they write in. A customer who sends a second message through the website while their first is still open no longer starts a second ticket. Their new words are added to the conversation they already have, in Hermes and in the help desk both, they are told the same reference number, and the conversation pops back to the top of the queue as unanswered. Closed conversations stay closed — a fresh question after that is a fresh ticket — and a thread that lives on Messenger or Instagram never absorbs an email. The help desk used to do this merge on its own, a second after Hermes created the ticket, and quietly deleted the copy Hermes was holding, so a reply from Hermes bounced off a ticket that no longer existed. Hermes now does the merge itself, reads the help desk's own merges the right way round, and a reply follows a merged conversation to wherever it went.
v2.107.0
Answers to the new website form now actually leave the building. The contact form went live on the store in the morning, and the first reply sent from Hermes never arrived — while the help desk cheerfully showed it as sent. The cause was a label. Hermes tags a message that came from the website as “contact form”, and when an agent replied, that tag was handed to the help desk as the way to send it. But “contact form” says where a message came from, not how to answer one, so the reply was filed away and no email was ever posted. Replies now go out as email, while conversations that genuinely live on Messenger, Instagram or live chat still answer on their own channel. Exactly one message was ever affected — the test that caught it, sent hours after the form went live and before a single customer had written in.
v2.106.0
The referral invitation gets a proper redesign. When a member shares their referral link, the friend who opens it now lands on a page that looks like the brand — the gold Flip My Life logo, warm cream and gold, and copy that says who sent the gift: “A gift from Chris — you've been invited to FLIP.” The friend enters their first name and their email (not just an email) and their one-time $15-off code appears, with a button straight to the store. There is no code to copy or type any more: the referral is the link, full stop, so the page where a friend used to type a code is gone and simply sends people to the store. Hermes now remembers the friend's first name alongside their email.
v2.105.0
The affiliate programme gets its own place in Hermes. Creator sends — the free product that goes out to creators, athletes and partners — has moved out of Fulfillment and into a new Affiliates section, because that work belongs to the person who runs the affiliate programme, not to the people who pack boxes. Old links to the fulfillment tab still open it, and the fulfillment team keep the desk they have always had. Beside it sits the full VIP list: every one of your best customers, richest first, whether or not somebody has already claimed them — the ordinary VIP list hides the claimed ones, and that is the wrong answer when you are looking for people to invite. You can search it, mark where each person stands (invited, signed up, said no) and filter by that mark, and a click on anyone opens their normal profile, so calls, outcomes, check-ins and notes are logged exactly the way they are logged everywhere else. Hermes also learned a new job title — Affiliate Manager — which an admin can hand out on the Team page.
v2.104.0
The account dashboard's round-8 doors. A friend handed the code from the end of a referral link can now type it in on the store's own referral page, and the dashboard shows both the link and the code. When a member writes a review from the dashboard, Hermes remembers which products they have reviewed. The "VIP tier upgrade" row in ways-to-earn is now what it always was — two spend milestones, 2,500 points at $300 lifetime and 5,000 at $750 — and the dashboard can tell which ones a member has crossed. Subscribers now earn the $300 / $750 spend-milestone bonuses going forward (Tyler, 2026-09-04: forward only, no retroactive grant). The follow-us rows on the dashboard can finally claim their points.
v2.103.0
The summary card at the top of a ticket can drop a wrong outcome too. The remove link that arrived on the tick line now also sits on the outcome rows of the case card above the messages, with the same confirm and the same Undo. Take it off there and the tick line and the Outcome button follow on their own.
v2.102.0
Launch eve for the account dashboard. Your referral link now starts with the store's own address. Returning a reward for points is safe in both directions: the points never come back while the code could still be spent, and a code the store has already lost still pays you back. Changing your sign-in email now asks for a code sent to the new address first. Members with boxes going to more than one address see one card per box and which card pays for each.
v2.101.0
Hermes stops reading the whole warehouse to answer one question. A handful of lookups behind the ticket tiles, the case card and the customer books were reading every shipment and every order in the store each time they ran, which is why the evening felt slow and a few store requests were turned away. Those lookups now go straight to the customer they are about, and three new indexes let the tiles jump to exactly the open tickets they need. Same numbers, same lists, a fraction of the work.
v2.100.0
The new account pages were put through a full adversarial review the same evening and every confirmed finding was fixed: when the store or the subscription system is down the dashboard says so instead of showing an empty page; a support message is never reported as sent unless a ticket really exists; a loading screen can no longer hand you the wrong answer; and a gift recipient at your own address is no longer mistaken for you.
v2.99.0
Recorded the wrong outcome or call? Remove it, and undo if you change your mind. A remove link now sits on every outcome and call log, on the ticket, on the customer's profile and in the activity log. It asks first, then takes the record out of every count and every card at once, and the message that confirms it carries an Undo. The person who recorded it, a lead or an admin can do it; the ticket itself stays exactly as it was.
v2.98.0
The account dashboard now tells your subscription apart from your store account. Addresses show which one your boxes ship to and which one checkout uses for one-time orders — with a tick box to update both at once, so nobody changes one thinking they changed the other. Payment methods open instantly and say plainly which card bills your subscription. Your information now holds first name, last name, email and phone. Customer Support is a message form that gives you a reference number, and the log-out button wears the theme's own colours.
v2.96.1
Outcome chips work end to end again. Tapping a chip on a ticket used to save the outcome and then blank the screen before you could say how it ended. The screen now stays put, shows the tick, and offers the "How did it end?" choices right away.
v2.96.0
Nothing in the account dashboard sends you somewhere else anymore. Your cards on file are listed right there — pick which one your box bills to, or ask for a secure link by email to add a new one (card numbers are never typed into the page). Your shipping address is edited in place and saves straight to your subscription.
stay in HQ
v2.80.0
The account dashboard stopped fibbing about status. A subscriber opening FLIP HQ was being told they were a base-level member who should spend $122 more to move up — when subscribing already puts you at the top tier. The tier is now worked out fresh on every visit from one set of rules, so subscribers see FLIP Fam, full bar, no upsell. And when the subscription service behind the page has an outage, the dashboard now says “we couldn't load your data, try again” instead of quietly pretending you have no subscription.
tier truth
v2.83.0
Voting in the monthly Top Flavor poll now pays 50 real points, the poll follows the calendar instead of being stuck on August, and last month's winner is counted from actual votes. Ratings and notes are now about the products you actually ordered, exclusive content is something the team adds from the theme editor with an unlock date, and every button that talks to the server stays busy until it is really done — no more silent removals.
real votes
v2.82.0
The account dashboard now prices things the way a subscriber pays them: every item in the box picker shows the subscription price, each flavor shows its own picture instead of the spinning all-flavors animation, and the reward on your next order can be removed, swapped or returned for points right from the subscription page. Ways to Earn marks what you have already claimed as done — and says when a yearly one opens again.
subscriber prices
v2.95.0
The half-hourly background run stops crashing, and the Rivo card stops crying wolf. Every half hour Hermes refreshes points from Rivo and re-scores who is at risk. The scoring used to hold every one of 120,000 customers in memory at once and ran out at the top of every hour; it now works in pages and produces exactly the same lists, checked side by side against the old maths. And where one slow answer from Rivo used to fail the whole refresh and turn the Rivo card red, each customer is now looked up on their own terms: a slow one is noted and skipped, the run carries on, and the card goes red only when Rivo is genuinely down.
v2.94.0
The store's front door now runs on Hermes: a contact form, a help center, and a chatbot we own. The contact form creates a real Hermes ticket and, quietly, its twin in Gorgias, so nothing changes for the team until we say so. Every FAQ the store publishes became a help-center article you can edit in Hermes, served on the store's own domain. And the chatbot answers from those articles and, once a customer is signed in, can do the things the account page already allows — skip a box, swap a product, apply rewards — each behind a confirm; anything bigger becomes a ticket for a person, and refunds are never on the table. It runs on our own AI key with the same daily cap and receipt as the card.
v2.93.0
The card can now read the conversation — with a receipt for every call. The problem in one line, the promises an agent made and when they were due, and how the customer's mood moved from first message to last, now come from the company's own AI key. Every call is logged with its cost, there is a daily spending cap you can change, and the one-time pass over the whole open book only runs when an admin presses the button and sees the estimate first. When the key is missing or the cap is hit, the card says so plainly instead of guessing.
v2.92.0
Every ticket now says where it stands — and whether it is stuck. A case strip on each conversation shows the problem, what has been done, the stage it is in (new, waiting on us, waiting on the customer, in progress, resolved) and a STUCK flag with the reason and how long: unanswered past the service window, the customer writing twice with no reply, a promised refund or reship with nothing to show for it, a ticket reopened again and again, or an orphan nobody owns. All of it comes from data Hermes already has, no AI guessing; the wording of the problem and the customer's mood come next. Leads get a Stuck tile, oldest first, and can mark a ticket "not stuck until" a date with a reason. Business hours are yours to set. Underneath, Hermes now hears about every change at Gorgias within minutes and copies conversations three times faster.
v2.91.0
Hermes now keeps its own copy of every conversation. Until now a ticket's messages lived only in Gorgias and were fetched each time someone opened one. Hermes is now copying all fifty-two thousand conversations, messages and internal notes into its own records at a pace Gorgias allows, on its own schedule, and reporting how much of the history agrees. Threads open instantly from that copy, internal notes are shown as notes, and a gauge on the Connections page shows the mirror's progress. This is the first step of running customer service from Hermes itself rather than through Gorgias.
v2.90.0
Every ticket and call now gets an outcome — in one tap. Closing a ticket asks one question, what was this about?, and a single tap on a chip records the answer and closes the ticket. Your last pick is remembered, so most closes are one press of Enter. Calls log the same way — reached, voicemail, no answer — and a voicemail never counts as a real check-in. Tickets closed inside Gorgias land on a catch-up tile so nothing slips through, and admins can rename or add chips in the app without a release. This is the foundation for the weekly customer-service numbers and the department report that follow.
v2.89.1
The loyalty checker got its own slot on the clock. It used to run last in a crowded half-hourly job and twice in one evening never got its turn. It now runs on its own schedule, so the re-reading of copied balances keeps moving, and every scheduled job now records how long each step took so the next slow one is easy to find.
:23 and :53
v2.89.0
The loyalty score now knows which balances Rivo actually confirmed. About a third of the balances behind the previous release's green light had been copied from our own books when members were linked, never read from Rivo. Every balance written from now on carries a label saying where it came from (older ones are assumed Rivo-read), copies count against the score until Rivo confirms them, the checker re-reads up to three hundred of them every half hour, and the "ready to switch Rivo off" checklist shows the pile as a blocker until it is gone. The tile goes honestly red for about a day and then means what it says.
37% → confirmed
v2.88.0
Adding a teammate now actually invites them. Adding someone used to save their account and then show an error, and nothing was ever emailed — the message that failed would have told them to "pick a password" that no longer exists. Now the new person gets an email in the same style as the sign-in code: who added them, which team and areas they have, and the one thing to do — open Hermes, enter this email, a code arrives. The roster confirms the invite went out (or says plainly that it did not, and why), and every row has a ✉ Invite button to send it again.
who · what · how
v2.87.0
The loyalty check stopped grading itself on a sample. The IT page said the points ledger was 95% right and showed red. It was 99.7% right: the checker only ever looked at the first 1,500 mismatches, found every one of them was simply Rivo's daily anniversary bonus waiting for a balance refresh, and then counted the rest it never looked at as failures. It now checks the whole book, spends its Rivo questions on the few members who actually need one, and says plainly what it found — "2,922 catching up · 83 never re-read · 2 open" — instead of "3,003 drifting". It also says, in the same breath, that about 9,700 of those balances were copied from our own books rather than read from Rivo, so nobody mistakes a green light for a Rivo-confirmed one.
95.14% → 99.7%
v2.86.0
The waits that showed nothing now say what they are doing. An outside check of the last release found the buttons that award points to hundreds of customers, change a batch of subscriptions, save a teammate's access or upload reference images all ran in silence — the screen simply did not change until the work finished. Each of those now shows a small centred card naming the job ("Awarding 500 points to 1,240 customers", "Skip next — 3 of 8"), counts where there is a real count, and greys the button so it cannot be pressed twice. Quick actions stay quiet: the card only appears if something is still running after just over half a second. Eleven screens that just said "Loading…" now say what they are loading, the sign-in code email carries the Hermes mark and no longer says "sign-in sign-in", and the report invitation's red button has readable text.
v2.85.1
Sixteen screens were checked to make sure each one says when it is loading. Fifteen already did. The one that did not — switching a category on the at-risk queue — used to leave the old list sitting there in silence; it now says "Loading…" right on the list, and the buttons wait until it lands.
15 of 16 → 16 of 16
v2.85.0
The rule that finally holds: the thing that is waiting shows the wait. No bar, no corner pop-up — each screen says it is loading in its own place, long jobs get a card centred on screen, and the first-open loading screen now wears the Hermes mark instead of the store's.
nothing ambient
v2.84.0
Two things. Hermes's emails now leave from Hermes's own address — name and sender both — and the invitation preview shows the real address it will use rather than a typed-in one. And the thin loading line across the top of the app is gone, replaced by a proper loading screen on first open and a small "Working…" pop-up only when something genuinely takes a while.
no-reply@hermes
v2.81.0
Nobody waits without knowing it any more. One thin bar at the top of the app appears only when something genuinely takes a moment — never for the quick things, never for the quiet background refreshes — and the long jobs show a named card with a live clock. When the app knows how many files it is importing, it says "3 of 7" instead of spinning. And there is a page where you can click every pattern and watch it work.
/progress.html
v2.80.2
Signing out used to leave the app clickable for a second while it worked — you could wander around a session that was already ending. Now a "Signing you out…" screen takes over the moment you click, and if the sign-out fails you are told so and left signed in, instead of being quietly signed back in by a reload.
no wait is silent
v2.80.1
Logging out never quite took: the app cleared its own cookie, but the shared sign-in that every Appolis app uses stayed alive, so a reload quietly signed you straight back in — and the code sign-in never got its turn. Log out now ends both, and the sign-in screen is guaranteed to be the first thing a signed-out person sees.
both cookies gone
v2.79.0
The password is gone. Signing in is now type your email, type the six digits we send you — nothing to remember, nothing to reset. And access stopped being a side effect of which department someone is in: an admin now ticks the areas each person can open, one profile at a time, so somebody in Marketing can run product sends while somebody in Fulfilment cannot.
no passwords
v2.78.0
Adding a teammate was impossible from inside the app — and the fulfilment screen turned out to be guarded only by being hidden. Twelve routes behind it, including the one that ships real product, asked nothing more than “are you signed in”. Fulfilment is now a real access level a person can hold without the page that stores the company's live credentials, and the Team tab finally has an Add person button.
12 routes gated
v2.54.0
The daily report stopped naming customers who were fine. Every loyalty “drifter” it flagged was a measurement artefact, and the arithmetic that proves it settled the bigger question: the ledger is not nearly right, it is right. The at-risk queue also stopped rebuilding itself inside a page load.
59,916 checked · 0 genuine
v2.53.0
The overnight slowdown, root-caused: one query read all 157,468 customer records on every incoming order, to catch the 19 people with a second email address. And every outbound email is now pinned to the support address — return labels had been going out from a login account.
132.7ms → 1.4ms
v2.52.0
Three production faults in one pass: replies to customers were structurally malformed and could fail in an agent's face; the health board pinned red for half a day after a problem had ended; and the loyalty tile read a fake 100% because its excuse window was seven days wide and forgave everyone.
443 failures found
v2.49.0 → v2.51.0
The bulk gifting board: stack conditions, watch the count and the dollar cost move, then award — and a re-run of the same campaign pays nobody twice. Alongside it, Tyler's fifty verdicts on the store's discount codes became data, so a ruling never needs a deploy again.
50 rulings live
v2.48.0 → v2.48.1
“I don't know why everything got broken.” Answered: every fulfilment webhook still pointed at a hostname retired in a rename weeks earlier, so carrier and shipment data had quietly stopped for 17 days. Repaired at both vendors, and the delivery report was caught recording the ship date as the delivery date.
16 webhooks wired
v2.45.0 → v2.47.0
The discount command deck: every code in the store on one screen, sorted into whose it is, with the money it moved. It immediately showed the biggest machinery had never been visible — the automatic volume discounts, one of them with 36,397 uses.
36,397 uses surfaced
v2.41.0
“Was this code spent?” had been unanswerable for subscription orders — Shopify's counter never moves when Recharge bills a charge. Four separate money guards were reading it, each one able to hand a customer the discount and their points back.
4 money guards
v2.39.0
Reward codes became single-use and owned. The pooled design had eight codes sharing a single use: the first customer to check out would have consumed the lot, and the other seven would have failed at the till after paying their points. Caught before it fired.
8 codes, 1 use
v2.29.0 → v2.38.0
Hermes mints the reward codes, and then learned to draw the rewards list the customer sees — because the vendor cannot render a code it did not create, which is exactly how people spent points for codes that appeared nowhere.
9 widget routes
v2.0.0 → v2.5.0
Flip-ready: the switch exists and everything behind it is built. Six days later the readiness gate went green with zero blockers. In the same stretch the storefront, the referral engine and the rewards page all came home, and Hermes got a page inside Shopify admin.
ready: true

Source of truth: APP_BREAKDOWN.md §13 — this deck syncs with it (docs-sync rule).

🐬 HERMES · v2.147.1

One team. One screen. One ledger.

Five services and 157,536 customers behind a single at-risk queue — plus every reward code in the store, the rewards page the customer sees, and a loyalty ledger checked against the whole book and found correct. The switch to flip is built and the gate is green. Throwing it is still one deliberate action, and it is Tyler's.

1,252 tests greenledger scored only on Rivo-confirmed balances · 11.4k copies re-reading~200ms queue344 releaseshermes.appolis.app
HERMES v2.147.1 · 1,252 tests green · hermes.appolis.app
Every figure is derived from APP_BREAKDOWN.md (§5 the engines, §7 the data, §13 changelog) or from the book measured 2026-08-17. In-app recreations use representative sample data.
Source of truth: APP_BREAKDOWN.md · deck regenerates with the doc