This repo already has an in-flight catalog migration — .github/agent-docs/plan-source-of-truth.md and .github/agent-docs/plan-catalog.md — which owns plan identity, entitlement numbers, and the Stripe price/product ledger; Stripe remains the sole authority for actual dollar amounts. This document is the broader map: every place across the codebase where pricing, plan names, or feature-limit numbers surface at all, including non-architectural touchpoints (docs, tests, marketing copy, assets) that the catalog migration doesn't itself track. Within each table, hardcoded/copy items that will need a manual touch are listed above reads_live_no_update_needed items, which are included for completeness/confirmation only.
| Location | Kind | Needs update when prices/tiers change? |
|---|---|---|
app/lib/l10n/app_en.arb (line 10581) planDeprecationMessage (+ app_fr.arb:2941, other locales) |
hardcoded_amount | Yes — literal "$49/mo" Operator price baked into localized copy; currently no Dart call sites found (may be dead) |
app/lib/l10n/app_en.arb (line 5843) monthlyPayoutsDescription |
hardcoded_amount | Yes, if the $10 payout threshold changes (app-developer payout copy, not subscriber pricing) |
app/lib/pages/settings/widgets/plans_sheet.dart (line 2083-2094) _tierGrantsDesktop |
config_value | Yes — client-side copy of desktop-entitled plan set; must mirror backend DESKTOP_ENTITLED_PLAN_TYPES |
app/lib/models/subscription.dart (line 86-88) PlanType.grantsDesktop |
config_value | Yes — second independent copy of the same desktop-entitlement mapping |
app/lib/models/subscription.dart (line 77-79) doc comment |
feature_limit_number | Yes (comment only) — "1500 min/month" for Plus will read stale if the real limit changes; runtime value itself is live |
app/test/utils/plan_pricing_test.dart (line 17-27,47-56) |
test_fixture | Yes (comments/docs) — fixtures encode today's real Plus/Unlimited/Neo prices; math functions themselves are price-agnostic |
app/test/unit/plans_sheet_l10n_test.dart (line 60-63,186,193-198) |
test_fixture | Yes — one assertion hardcodes formatted string "$161.91" (today's real Plus annual price) |
app/lib/l10n/app_en.arb (line 10668,10676,10684) neoSubtitle/operatorSubtitle/architectSubtitle |
plan_name_or_tier_copy | Yes, if resurrected — currently no call sites found outside generated l10n files |
app/lib/pages/settings/widgets/plans_sheet.dart (line 1569,2086-2094) tierOrder list |
plan_name_or_tier_copy | Yes — hardcoded canonical plan-ID list; new/retired plan IDs require updating this literal |
app/lib/pages/settings/widgets/plans_sheet.dart plan titles/prices/features (~2043-2075, 1671-1697) |
reads_live_no_update_needed | No — sourced live from backend availablePlans/planData |
app/lib/utils/plan_pricing.dart (whole file); app/lib/pages/settings/usage_page.dart (line 1030-1059) |
reads_live_no_update_needed | No — discount badges and usage/quota figures derive from live monthly/yearly unit amounts and subscription response |
| Location | Kind | Needs update when prices/tiers change? |
|---|---|---|
desktop/macos/Desktop/Sources/MainWindow/QueryShell/QueryShellHome.swift (line 198-206) |
hardcoded_amount | Likely dead, but Yes if reachable — stale "$199/month Omi Pro" alert, plan name doesn't match any current tier |
desktop/macos/Desktop/Sources/Providers/ChatProvider.swift (line 3158,4555,5566) |
hardcoded_amount | Yes — literal $50 free-tier spend cap duplicated at 3 call sites |
...Settings/Components/SettingsContentView+BillingHelpers.swift:34 (comment) |
hardcoded_amount | Yes (comment) — "Neo ($20) | Operator ($49) | Architect ($200)" |
...BillingHelpers.swift:141-152 planSubtitle |
feature_limit_number | Yes — fallback subtitle question counts, only shown when catalog omits subtitle. Now static for testability |
...BillingHelpers.swift:195-206 planDescription |
feature_limit_number | Yes — fallback descriptions; internal inconsistency (100 vs 200 for "unlimited") fixed in this PR. Now static for testability |
...BillingHelpers.swift:243-269 fallbackFeatures |
feature_limit_number | Yes — includes literal "~$400 of monthly AI compute" and question counts. Now static for testability |
...Sections/SettingsContentView+AccountBilling.swift:342-345 |
hardcoded_amount | Fixed in this PR — was a bare "$49/mo" literal, now reads operatorDeprecationFallbackPrice, a single named constant |
...BillingHelpers.swift:182-193 planEyebrow |
plan_name_or_tier_copy | Yes — fallback marketing eyebrow text per plan id. Now static for testability |
...BillingHelpers.swift:288-324 planCatalog(from:) + normalizedPlanId (271-286) |
plan_name_or_tier_copy | Yes — fallback title mapping/keyword matching for plan display names |
...Sections/SettingsContentView+AccountBilling.swift:102-128 |
other | Only if resurrected — dead/commented "Upgrade to Pro" card with stale marketing URL |
desktop/macos/Desktop/Sources/Services/APIClient/APIClient+Settings.swift (line 370) (comment) |
hardcoded_amount | Yes (comment only) — "$400/mo" example for Architect |
desktop/macos/Desktop/Sources/FloatingControlBar/FloatingBarUsageLimiter.swift (line 15-27) proactiveBudgetMultiplier |
config_value | Yes, if tiers/entitlements restructured — plan-tier-keyed multiplier constants |
TrialBannerService.swift:88 |
hardcoded_amount | Yes — "3-day premium trial" string doesn't interpolate from trialDurationSeconds |
desktop/macos/Desktop/Sources/AppState/AppState+TrialPaywall.swift (line 121-166) (#if DEBUG) |
test_fixture | Yes, if kept accurate — debug-only trial mock, not shipped to release |
desktop/macos/Desktop/Sources/MainWindow/SettingsSidebar.swift (line 187-198) |
plan_name_or_tier_copy | Yes — settings-search subtitles/keywords name "Operator"/"Architect"/"unlimited" |
desktop/macos/Desktop/Sources/MainWindow/SettingsSidebar.swift (line 200-204) |
plan_name_or_tier_copy | Yes — referral search subtitle names "Operator" |
desktop/macos/Desktop/Sources/MainWindow/Referrals/ReferralProgramView.swift (line 64,69) |
plan_name_or_tier_copy | Yes — referral header text hardcodes "Operator" and "one month" |
desktop/macos/Desktop/Tests/SubscriptionPlanCatalogMergerTests.swift (line 1-58) |
test_fixture | No — arbitrary test doubles, not real prices |
desktop/macos/Desktop/Tests/SubscriptionPlanPresentationTests.swift (line 1-27) |
test_fixture | No functionally, but coincidentally matches real Operator price — worth a glance |
desktop/macos/Desktop/Tests/SubscriptionInfoDecoderTests.swift (line 1-186) |
test_fixture | Only the deprecation-message fixture needs to track the real fallback string |
desktop/macos/Desktop/Tests/FloatingBarUsageLimiterTests.swift (line 41-238) |
test_fixture | Yes — quota fixtures (Architect $400 cap) should track real limits to stay meaningful |
desktop/macos/Desktop/Sources/VADGateService.swift (line 483) |
config_value | Yes, if Deepgram's per-minute cost changes — unrelated to Omi plan pricing |
...AccountBilling.swift:414,422-435,479-494 overage card |
reads_live_no_update_needed | No — all figures from live OverageInfoResponse |
desktop/macos/Desktop/Sources/Services/APIClient/APIClient+Settings.swift (line 411-609) displayName/price types |
reads_live_no_update_needed | No — price fields populated live; only plan renames/new IDs touch displayName |
desktop/macos/Desktop/Sources/UsageLimitPopupView.swift |
reads_live_no_update_needed | No — generic copy, no numbers |
desktop/macos/Desktop/Sources/FloatingControlBar/FloatingBarUsageLimiter.swift (line 147-155) limitDescription |
reads_live_no_update_needed | No — reads live server quota object |
| Location | Kind | Needs update when prices/tiers change? |
|---|---|---|
desktop/windows/src/renderer/src/lib/billing.ts (line 362-399) PLAN_FALLBACKS |
feature_limit_number | Yes — fallback eyebrow/subtitle/description/features; 100-vs-200 "unlimited" inconsistency fixed in this PR |
.../components/settings/tabs/PlanUsageTab.tsx:228-230 |
hardcoded_amount | Fixed in this PR — was a bare "$49/mo" literal, now reads OPERATOR_DEPRECATION_FALLBACK_PRICE, a single named constant |
desktop/windows/src/renderer/src/lib/billing.test.ts (line 54-68,167-176,252,358-372,546-547,568) |
test_fixture | Yes — full mock catalog with plan titles/prices baked into assertions |
desktop/windows/src/renderer/src/lib/chatQuotaGate.test.ts (line 16,46,59,187) |
test_fixture | Yes, if display names/quota model change |
desktop/windows/src/renderer/src/lib/billingPlans.ts (line 94-101) PLAN_DISPLAY_NAMES |
plan_name_or_tier_copy | Yes — client-side authority for plan display names |
desktop/windows/src/renderer/src/lib/billingPlans.ts (line 20,80-86) alias/paid-ID sets |
config_value | Yes, if plan IDs/aliases change |
desktop/windows/src/renderer/src/lib/billing.ts (line 314-320,434-437) PLAN_ORDER/canPurchasePlan |
config_value | Yes — hardcoded display order and downgrade-block business rule |
desktop/windows/src/renderer/src/lib/billing.ts (line 68) LEGACY_PLAN_TITLES |
plan_name_or_tier_copy | Only if the legacy-catalog canary titles themselves change |
.../components/apps/AppDetailSheet.tsx:160,279,304 |
other | No — marketplace app price is live, distinct pricing surface (not subscription plans) |
billing.ts fetch* functions + PlanGrid/CurrentPlanCard/ChatUsageCard/OverageCard render paths |
reads_live_no_update_needed | No — plan titles, prices, usage, trial, overage all read live from backend |
| Location | Kind | Needs update when prices/tiers change? |
|---|---|---|
web/app/src/components/settings/SettingsPage.tsx (line 969-975,1185) limits object |
feature_limit_number | Yes — hardcoded Basic free-tier caps instead of reading from UserSubscriptionResponse |
SettingsPage.tsx:1171,1176-1177,1203 |
feature_limit_number | Yes — literal "1,200 min" duplicated 3x for free-tier listening limit |
SettingsPage.tsx:1133-1135,1143,1212-1234 |
plan_name_or_tier_copy | Yes — Basic-plan "what's included" marketing copy is a silent duplicate of catalog entitlements |
web/app/src/lib/planFeatures.ts DEFAULT_PLAN_FEATURES |
plan_name_or_tier_copy | Fixed in this PR — was two independent hardcoded copies (SettingsPage.tsx and PlansSheet.tsx), now one shared constant here |
web/app/src/types/user.ts (line 134-147) planDisplayName |
plan_name_or_tier_copy | Yes — must stay in sync with plan_catalog.json naming |
web/app/src/lib/api.ts (line 2014-2017) SegmentEditPlanRequiredError |
plan_name_or_tier_copy | Yes — hardcodes "Unlimited plan" as the gating tier |
web/app/src/app/login/LoginClient.tsx (line 234) |
plan_name_or_tier_copy | Yes — referral headline hardcodes "Operator", independent of backend grant logic |
web/admin/lib/stripe-subscriptions.ts (line 49-56) OMI_PLAN_PRODUCTS |
plan_name_or_tier_copy | Yes — sole source of plan identity for admin revenue/subscription metrics; new Stripe product invisible until added |
web/admin/app/api/omi/stats/subscriptions/route.ts (line 64), .../stats/revenue/route.ts:63 |
other | Yes — both depend on the same OMI_PLAN_PRODUCTS map above |
web/admin/lib/__tests__/stripe-subscriptions.test.ts (line 293-302) |
test_fixture | Yes — independent literal array of 6 plan display names must track the map |
web/app/src/types/__tests__/userPlan.test.ts (line 51-62) |
test_fixture | Yes — literal array of plan IDs; adding a 7th plan requires updating |
web/app/src/types/user.ts (line 65-72,96-102) CATALOG_PLAN_IDS/PAID_CATALOG_PLAN_IDS |
reads_live_no_update_needed* | Yes, but this IS the canonical single-source client update point (not a stray duplicate) |
web/app/src/lib/api.ts (getAvailablePlans, getUserSubscription, etc.) |
reads_live_no_update_needed | No — all fetched live from backend/Stripe |
web/admin/.../dashboard/subscriptions/page.tsx |
reads_live_no_update_needed | No — MRR/ARR/amounts computed live |
web/admin/app/api/omi/stats/* routes |
reads_live_no_update_needed | No, except shared dependency on OMI_PLAN_PRODUCTS noted above |
web/app/src/lib/omiApi.generated.ts |
reads_live_no_update_needed | No, provided codegen is re-run when the backend schema changes (unverified — see gaps) |
web/admin/lib/utils/user-subscription.ts |
reads_live_no_update_needed | No — generic passthrough helpers |
| Location | Kind | Needs update when prices/tiers change? |
|---|---|---|
web/frontend/src/app/components/product-banner/types.ts (line 23) PRODUCT_INFO.price |
hardcoded_amount | Yes, if the $89 hardware price changes (device price, not subscription) |
web/frontend/src/app/apps/utils/metadata.ts (line 104) |
hardcoded_amount | Yes — independent duplicate of the $89 device price for SEO JSON-LD |
web/frontend/src/app/apps/[id]/page.tsx (line 118) |
hardcoded_amount | Yes — third independent duplicate of the $89 device price |
web/frontend/src/app/unlimited/page.tsx (line 6,39,41) |
plan_name_or_tier_copy | Yes — stale "Omi Unlimited" landing page name vs. current catalog naming (unlimited_v2 vs deprecated Neo) |
src/__tests__/wrapped-unlimited-deeplink-parity.test.mjs |
test_fixture | Yes — static string tripwire on the /unlimited route name, breaks if route is renamed |
web/frontend/src/app/apps/utils/metadata.ts (line 214-215) generateAppListSchema |
other | No — correctly hardcoded $0 for free-to-list marketplace apps |
web/frontend/src/app/create-app/page.tsx (line 49,294,310,366,774-859) |
reads_live_no_update_needed | No — third-party developer's own app-pricing form field, unrelated to Omi plans |
public/ images (omi_1.webp, etc.) |
other | Unknown — not visually inspected for baked-in price text |
web/frontend/src/app/page.tsx |
reads_live_no_update_needed | No — redirects to /apps, no pricing content |
| Location | Kind | Needs update when prices/tiers change? |
|---|---|---|
backend/config/plan_catalog.json (line 115-570) allocations, :12-76 allocation_profiles |
feature_limit_number | Yes — this IS the canonical place to edit finite quotas/budgets |
backend/utils/subscription.py (line 474-535) get_paid_plan_definitions |
plan_name_or_tier_copy | Yes — hardcoded storefront titles/eyebrows/subtitle/description text |
backend/utils/subscription.py (line 484,496) annual_description |
hardcoded_amount | Yes — "Save ~17% with annual billing." baked into Neo/Operator copy, not computed from live Stripe ratio; lines 508 (architect), 520 (plus), 532 (unlimited_v2) say "Save with annual billing." (no percentage) |
backend/utils/subscription.py (line 987-1002) _chat_allowance_text |
config_value | Yes (via editing plan_catalog.json) — derives "$400" from catalog usd_cent value; live-read but easy to mistake for Stripe-sourced |
backend/routers/payment.py (line 196) docstring example |
localized_copy | Cosmetic only — illustrative comment, not runtime |
backend/charts/backend-listen/{dev,prod}_*_values.yaml, backend/charts/pusher/{dev,prod}_*_values.yaml |
config_value | Yes — Helm env vars duplicate quota overlays and Stripe price IDs for legacy plans; must update both dev+prod together |
Chart files' SUBSCRIPTION_LAUNCH_DATE |
config_value | Yes, if the cutover date changes — duplicated across 4 files |
backend/deploy/runtime_env/prod.overlay.yaml (line 185-192), backend/deploy/runtime_env.yaml (line 1603-1610) |
config_value | Yes — literal Plus/Unlimited-v2 Stripe price IDs (prod only), must track plan_catalog.json's recognized_stripe_prices |
backend/deploy/runtime_env/dev.overlay.yaml (absence) |
config_value | Confirm intentional — no dev price IDs for Plus/Unlimited-v2 today |
config/deployment-setting-classification.json (line 172-175) |
config_value | Yes, if new plan price env-var names are introduced (need a classification entry) |
backend/config/plan_catalog.json (line 571-712) recognized_stripe_prices/products |
config_value | Yes — append-only ledger; new Stripe prices for new/changed tiers must be appended here |
.github/agent-docs/plan-source-of-truth.md (line 405-421) (open ledger gap) |
other | Yes — pre-existing unresolved discrepancy between a test fixture and the dev ledger for Architect |
backend/tests/unit/test_available_plans_resilience.py (line 19-22,111) |
test_fixture | Yes, if the dev ledger gap above is resolved |
backend/tests/unit/test_overage_catalog.py (line 8-16,29-42) |
test_fixture | Yes — hardcoded per-plan hard-cap-vs-overage policy and quota numbers (500/$400/200) mirror catalog |
backend/tests/unit/test_subscription_restructure.py (line 1-2,88-98,510-515) |
test_fixture | Yes — stale $49/$400 figures in docstring, plus hardcoded display-name assertions |
backend/utils/subscription.py (line 620-630,684-685) version gates |
config_value | Yes, if a pricing change ships alongside a client capability gate |
backend/routers/payment.py (line 478-522) price_string/unit_amount |
reads_live_no_update_needed | No — live stripe.Price.retrieve at request time |
backend/utils/subscription.py (line 1038-1039) get_plan_display_name |
reads_live_no_update_needed | No — reads generated PLAN_DISPLAY_NAMES |
backend/config/plan_catalog_generated.py |
reads_live_no_update_needed | No — generated artifact, never hand-edited |
backend/utils/stripe.py, backend/scripts/support/find_stripe_entitlement_mismatches.py, backend/config/plan_catalog.py |
reads_live_no_update_needed | No — generic helpers/facades with no hardcoded literals |
| Location | Kind | Needs update when prices/tiers change? |
|---|---|---|
docs/api-reference/app-client-openapi.json (line 50708) (from backend/routers/payment.py docstring) |
plan_name_or_tier_copy | Yes — stale "Unlimited→Pro" example; fix the source docstring, not the generated doc |
web/admin/docs/data-contracts.md (line 20) |
config_value | Yes — procedural instruction to add a line to OMI_PLAN_PRODUCTS when launching a plan |
docs/doc/developer/mcp/tools.mdx (line 225) |
feature_limit_number | Yes, if the free-tier preview length changes — hardcoded "70 characters" |
docs/doc/developer/mcp/tools.mdx (line 202,223), docs/doc/developer/mcp/troubleshooting.mdx (line 59-61) |
plan_name_or_tier_copy | Only if the free/paid gating boundary itself changes — generic "paid plan" language |
docs/doc/developer/apps/Oauth.mdx (line 220) |
other | Only if marketplace-app monetization scope changes — separate pricing system from the 6-plan catalog |
.github/agent-docs/web-app-destinations.md (line 14) |
other | No — pointer doc only, no names/prices |
docs/api-reference/app-client-openapi.json (other endpoint descriptions) |
reads_live_no_update_needed | No — auto-generated mirrors of backend docstrings |
backend/AGENTS.md (line 16) |
reads_live_no_update_needed | No — describes live-validation behavior |
docs/doc/developer/backend/Backend_Setup.mdx (line 86) |
reads_live_no_update_needed | No — generic setup instructions |
| Location | Kind | Needs update when prices/tiers change? |
|---|---|---|
app/assets/images/neo_one.webp |
other | No (probable false positive) — appears to be device-pairing art, not the "Neo" plan; worth a quick visual double-check |
app/assets/images/ic_dollar.svg, ic_clone_plus.svg, ic_clone_chat.svg |
other | No — decorative icons, no baked-in numbers |
app/assets/competitor-logos/limitless-logo.jpg, app/assets/images/limitless.png |
other | No — competitor branding, unrelated to Omi pricing |
web/app/public/app-store-badge.svg, google-play-badge.png (+ web/frontend, docs equivalents) |
other | No — generic store-badge artwork |
.github/issue-assets/6559-gh-plan-usage*.png |
other | No — historical issue screenshots, not shipping product, unreferenced in-repo |
| Surface | Confirmed absent from repo | System of record to check |
|---|---|---|
| App Store listing copy (subtitle, description, price tier mentions) | No fastlane/, App Store Connect metadata, or app-store/appstoreconnect directories found anywhere |
App Store Connect |
| Google Play listing copy (short/long description, pricing mentions) | No play-store/playstore directories or Android fastlane/metadata found |
Google Play Console |
| Paid ad creative mentioning price points | No ads/ directory anywhere in the repo |
Whatever ad platform(s) run Omi's paid campaigns (e.g. Meta/Google Ads) |
| Social media copy/captions mentioning pricing | No socials/ or marketing-copy directory anywhere in the repo |
Whatever social scheduling tool the growth/marketing team uses |
| Blog/CMS content mentioning pricing | No MDX/blog content directory found under web/frontend/src/app or docs/ |
Whatever CMS or blog platform hosts Omi's blog, if one exists outside this repo |
- Done in this PR: the two internal inconsistencies in shipped fallback copy (
SettingsContentView+BillingHelpers.swiftand its Windows twinPLAN_FALLBACKSinbilling.tsboth said 100 in one place and 200 in another for "unlimited"/Neo's monthly question count — 200 is correct perplan_catalog.json); the hardcoded "$49/mo" in macOSAccountBilling.swiftand WindowsPlanUsageTab.tsxdeprecation-banner fallbacks, now each a single named constant; and the two independent copies ofweb/app's genericdefaultFeatureslist, nowweb/app/src/lib/planFeatures.ts. The mobile ARBplanDeprecationMessagestring was deliberately left as-is — no confirmed Dart call site, and rewriting it would touch ~49 untranslated locale files. - Do not touch actual dollar literals yet (Stripe price IDs,
usd_centallocations inplan_catalog.json, chart/env price IDs) until final tier numbers are decided — those are single-edit-point changes by design and premature edits risk drifting from the still-open Architect dev-ledger gap tracked inplan-source-of-truth.md. - Resolve the open Architect dev Stripe price-ID ledger gap (
test_available_plans_resilience.pyvsplan_catalog.json) before shipping any new pricing that touches dev testing — verify against the dev Stripe dashboard first, per the doc's own caveat. - Flag the stale "Unlimited→Pro" example in the
backend/routers/payment.pydocstring as a quick fix independent of the pricing rollout — it's leaking a non-existent plan name into public OpenAPI docs today. - Before finalizing new prices, confirm whether
web/app/src/lib/omiApi.generated.ts's codegen step actually runs onplan_catalog.jsonchanges — if it's manual, add it to the release checklist now so the rollout doesn't ship stale generated types. - Treat the $89 hardware price and App Store/Play Store/ad/social copy as explicitly out of scope for the subscription pricing migration, but assign an owner to sweep the three duplicated $89 code locations and the external consoles/tools listed above in the same rollout window so nothing is silently missed.