Skip to content
All skills
OPERATOR ops v1.0.0 · Apache-2.0

Stripe Live Price Rotation

Rotate live Stripe prices on a running production product (e.g. Scan free / Rescue $7 / Ward $29/mo) without breaking checkout. Covers the archive-not-delete reality, the dual env-var coupling (modern + legacy alias), the multi-file frontend copy hunt, the system-vs-user systemd detection, and the JS-bundle-not-just-HTML smoke test. Use when Adam asks "set the pricing to X/Y/Z" on a Stripe-wired product and the change must go live in one session. Validated 2026-04-28 on rescue.wisechef.ai (8min end-to-end).

Audited
Source
SHA-256
Last reviewed
How we audit →

Install in your agent

Tell your agent: "install the recipes skill, then add stripe-live-price-rotation"
Or via curl: curl -sL https://recipes.wisechef.ai/skill -o ~/.claude/skills/recipes/SKILL.md

Full skill source · SKILL.md

stripe-live-price-rotation

TL;DR

Wrong skill? If the founder wants to temporarily flip a price to €0.00 for an end-to-end checkout walkthrough (no real money moves, revert in 90s), use stripe-e2e-test-price-swap instead. This skill is for permanent pricing changes.

Stripe prices are immutable. To "change a price" you create a NEW price object on the same product and archive (active=false) the old one. Old subscribers stay on the old price; new Checkout sessions can only use active prices. The hard parts are not Stripe — they're (a) finding every place the price lives in your codebase, (b) the legacy env-var name your old code still reads, and (c) verifying the new JS bundle on the CDN actually has the new copy.

When to use

  • Adam: "set scan free, apply fixes for $7, ward mode for $29/mo"
  • Adam: "drop the price on X to Y"
  • A pricing experiment requires a fast rollback path (you can flip active=true/false on archived prices to revert)

Required context before starting

  • Stripe secret key (sk_live_… or sk_test_…) — usually in product .env on the host
  • The product IDs (prod_…) for each tier — Stripe dashboard or GET /v1/products
  • Whether the host runs the service as system systemd (sudo systemctl) or user systemd (systemctl --user) — check first, don't assume
  • Branch name + worktree path of the frontend repo
  • What env-var names the backend reads — grep for both modern (PRICE_X) and legacy (X_PRICE_ID) forms

The 8-step procedure

1. List current state (sanity check)

ssh $HOST 'set -a; source /path/to/.env; set +a;
  curl -s -u "$STRIPE_SECRET_KEY:" "https://api.stripe.com/v1/products?limit=20" | \
    python3 -c "import json,sys; d=json.load(sys.stdin); [print(p[\"id\"],p[\"name\"]) for p in d[\"data\"]]"
'

For each product you're touching, list its prices:

curl -s -u "$STRIPE_SECRET_KEY:" "https://api.stripe.com/v1/prices?product=prod_X&limit=10" | \
  python3 -c "import json,sys; d=json.load(sys.stdin); [print(pr['id'],pr.get('unit_amount'),pr['currency'],pr.get('recurring',{}).get('interval','one-time'),'active='+str(pr['active'])) for pr in d['data']]"

Capture the OLD price IDs — you'll archive them in step 3.

2. Create new prices

One-time:

curl -s -u "$STRIPE_SECRET_KEY:" https://api.stripe.com/v1/prices \
  -d "product=prod_X" \
  -d "unit_amount=700" \
  -d "currency=usd" \
  -d "nickname=Rescue \$7 (2026-04-28)"

Recurring monthly:

curl -s -u "$STRIPE_SECRET_KEY:" https://api.stripe.com/v1/prices \
  -d "product=prod_Y" \
  -d "unit_amount=2900" \
  -d "currency=usd" \
  -d "recurring[interval]=month" \
  -d "nickname=Ward \$29/mo (2026-04-28)"

Capture the new price_… IDs. Use a dated nickname so future-you can identify them in the dashboard.

3. Archive old prices

curl -s -u "$STRIPE_SECRET_KEY:" "https://api.stripe.com/v1/prices/$OLD" -d active=false

⚠️ Stripe prices cannot be deleted. Archived = no new Checkout sessions can use them. Existing subscriptions on that price keep billing at the old rate until manually migrated. If you have live subscribers, plan the migration separately (Stripe Subscriptions API update to swap to new price; usually proration applies).

4. Update backend env

Edit the .env on the production host (NOT the worktree — production is the source of truth for env vars):

sed -i "s|^PRICE_RESCUE=.*|PRICE_RESCUE=price_NEW|" /path/to/.env
sed -i "s|^PRICE_WARD=.*|PRICE_WARD=price_NEW|" /path/to/.env

The legacy alias trap. If the codebase has both PRICE_RESCUE (modern) AND RESCUE_PRICE_ID (older naming from earlier sprints), you MUST update both — checkout-routes.js may still read the old one. Grep for it:

ssh $HOST 'grep -rE "process\.env\.[A-Z_]*PRICE[A-Z_]*" /path/to/app/src/'

5. Patch frontend copy

Pricing copy lives in MANY places. Don't trust just PricingCards.jsx. Hunt all of:

  • src/components/PricingCards.jsx — tier price field, period, tagline, sub-copy, footer note
  • src/components/LandingPage.jsx (or hero component) — hero CTA buttons, hero sub-paragraph
  • src/pages/Pricing.jsx (or /pricing route) — page h1 sub-copy
  • src/components/ReportPage.jsx (or post-checkout component) — inline upsell CTAs
  • index.html<meta name="description"> and OG tags (SEO impact)
  • Tests: src/components/__tests__/PricingCards.test.jsx — hardcoded snapshot strings

Fast grep:

cd $FRONTEND_DIR && grep -rnE "\\\$7|\\\$29|\\\$79|\\\$N|specific-old-words" \
  --include="*.jsx" --include="*.tsx" --include="*.html" --include="*.md" src/ index.html

6. Build + deploy

cd $FRONTEND_DIR && npm run build
# Verify no errors and the bundle changed:
ls dist/assets/PricingCards*.js   # new hash should differ from current prod
rsync -avz --delete dist/ $HOST:/path/to/panel-dist/

7. Restart backend (system OR user systemd)

Detect first, don't assume:

ssh $HOST 'sudo systemctl list-units --type=service --all --no-pager 2>/dev/null | grep -iE "<service>"'

If found there → it's system-level:

ssh $HOST "sudo systemctl restart $SERVICE; sleep 2; sudo systemctl status $SERVICE --no-pager | head -15"

If not found → check user systemd (requires lingering or active session):

ssh $HOST "loginctl list-users; sudo -u $USER XDG_RUNTIME_DIR=/run/user/\$(id -u $USER) systemctl --user list-units --type=service"

8. Smoke test (JS bundle, not just HTML)

Pricing copy compiles into hashed JS chunks. The HTML may be stale-cached even when the bundle hash changed. Verify both:

# 1. Healthz
curl -s -o /dev/null -w "HTTP %{http_code}\n" https://$DOMAIN/api/healthz

# 2. Landing HTML has new strings
curl -s https://$DOMAIN/ | grep -oE "(NEW_PRICE|new copy)" | sort -u

# 3. Pricing page returns 200
curl -s -o /dev/null -w "HTTP %{http_code}\n" https://$DOMAIN/pricing

# 4. CRITICAL: the JS chunk has new prices, no old leftovers
NEW_HASH=$(curl -s https://$DOMAIN/ | grep -oE 'PricingCards-[A-Za-z0-9_-]+\.js' | head -1)
curl -s "https://$DOMAIN/assets/$NEW_HASH" | grep -oE "(\\\$NEW|\\\$OLD)" | sort -u
# Should show NEW values only. ANY $OLD leftover means a file you forgot to patch.

9. Commit + document

cd $FRONTEND_DIR
git add -A src/
git commit -m "feat(pricing): <new structure>"
git push --set-upstream origin <branch>

Obsidian status doc:

  • projects/<product>/<date>-pricing-reset.md with: directive quote, new vs old table, archived price IDs, files changed, commit hash, smoke-test results, what's intentionally NOT done (e.g. marketing-site copy in a separate repo)
  • Append to log.md
  • Update index.md to link the new doc, mark older pricing-research notes as superseded

Discord post to home channel: directive received, what shipped, what's left from prior sprint status (don't conflate the pricing change with unrelated open items, but DO surface them so Adam doesn't have to re-read three docs).

Pitfalls (validated)

  1. The "Pricing.jsx accidentally double-wrapped" — when patching with x_patch and the surrounding context is generic styling, the diff can land inside an existing <p> tag. After patching, re-read each file you touched and confirm structure. If you nest a <p> inside a <p>, React will hydrate-error in production. Verify by npm run build AND by viewing the page after deploy.

  2. The legacy env-var alias — Adam's products have accumulated env naming history. PRICE_RESCUE (sprint 1.5+) coexists with RESCUE_PRICE_ID (earlier code). Setting only one breaks the other code path silently. Always grep the codebase for process.env.*PRICE* before deciding which vars to update.

  3. systemctl --user fails on services that turned out to be system-level. rescue-medic specifically: SOUL/sprint docs may say "user systemd-managed" but the unit is in /etc/systemd/system/. Check sudo systemctl list-units first.

  4. JS bundle hash changes on rebuild — old bundle stays cached on the CDN/CF edge briefly. The MD5/hash-named bundle in the new HTML reference is the truth. Curl the HTML, extract the hash, then curl that exact bundle. Don't trust browser-side smoke testing without a hard refresh.

  5. Archived ≠ removed. If you screwed up and need to revert, you can re-activate the archived price (active=true) — but if you've already deleted env references and the codebase doesn't know the old ID, you have to either restore the old ID or re-create yet another new one. Save the OLD price IDs in your status doc for at least a sprint.

  6. Subscription products have a migration cost. Setting active=false on a Ward $79/mo doesn't affect existing $79/mo subscribers — they keep paying $79 forever until manually migrated. If migration is intended, do it deliberately via the Stripe Subscriptions API (POST /v1/subscriptions/<id> with items[0][price]=$NEW_ID and proration_behavior=create_prorations). For rescue.wisechef.ai 2026-04-28 there were zero existing Ward subscribers so this was a non-issue — capture the count before assuming.

  7. payment-guard.js dev-mode bypass. Some backends have a hardcoded amount: 700 literal in dev/bypass paths (rescue-medic does at payment-guard.js:109). If you flip Rescue to a different price, that literal is now lying in dev mode. Either update it or accept that dev-mode amount no longer reflects production — but DECIDE, don't ignore.

Smoke-test minimal checklist (post-deploy)

  • /api/healthz 200
  • / 200, contains expected new copy
  • /pricing 200
  • New JS bundle hash present in HTML (not the old one)
  • New JS bundle contains only new prices (no $OLD leftovers)
  • Service active (systemctl status) AND has been up since AFTER the env edit
  • Backend logs show no errors on first checkout attempt (tail journal for 30s)
  • (Optional) Run a real Stripe Checkout in test mode to verify the price renders correctly in the Stripe-hosted page

Reference: validated 2026-04-28 on rescue.wisechef.ai

  • Adam directive at 06:50 → live at 06:55 (8 min E2E)
  • Stripe products kept (prod_UPmpM16Na320EQ Scan, prod_UPmpflAs0uKmaf Rescue, prod_UPmptqbjkwX5Lk Ward, prod_UPmpnRsNUVobvF ICU)
  • 3 prices archived (Scan $7, Rescue $29, Ward $79/mo); 2 created (Rescue $7 = price_1TR3a7Egmqt5xoaLGVKQ04IP, Ward $29/mo = price_1TR3a7Egmqt5xoaLiWc8fn32)
  • Scan tier: kept the product, marked PRICE_SCAN=free (app layer never reads it for charging — gated only by professional_capacity boolean)
  • 4 frontend files patched, 1 build, 1 rsync, 1 service restart, 4 smoke checks, 1 commit, 1 status doc, 1 Discord post
  • Status doc: projects/wisechef/rescue/2026-04-28-pricing-reset.md
  • Commit: b6f7f48 on agent/tori/rescue-sprint1-frontend