The company, in one paragraph
AstroMatrix is a profitable, deeply-built astrology product — roughly $17–20K/month, 87,467 MAU, 4.6★ — whose Android install base has declined about 35% over four years, steadily. Three years of real product depth work were effective at what they targeted, but no feature ever had user-creation as its explicit job.
Separately, the loyal base that remains is badly monetised (5.37% ever paid, $0.20 ARPMAU) — largely because the paths to pay were structurally broken, not because demand is absent. Those are two distinct problems, a growth-engine failure and a packaging failure. They have separate fixes and should not be collapsed into one narrative.
The single company question this quarter: can AstroMatrix generate users from users?
The five lanes
There is no single project here. There are five parallel lanes that each acquired their own tracker — which is the actual source of the “it’s all over the place” feeling.
| Lane | Where it lives | Its tracker | State |
|---|---|---|---|
| A · Product & growth strategy | flutter/docs/, nodejs/docs/product/ | FEATURE-LIST.md | converged |
| B · Engineering fires | nodejs/docs/SUPPORT-fire-* | master fire report | ~147 issues |
| C · Website & SEO | astromatrix-public/ | memory: SEO audit | audit done, queue open |
| D · Paid ads & economics | astromatrix-public/docs/ | SESSION-HANDOFF | day-3 read pending |
| E · Infra, DR & migration | nodejs/docs/ | INFRA-*, MIGRATION-* | on a 3–6 mo clock |
Lanes A and B are the same product at two altitudes. A fire-report ISSUE number is the engineering half of most roadmap items — roadmap “link the store” is ISSUE-76. C and D are the acquisition side and barely touch A/B. E is a clock: the legacy .NET database goes offline in 3–6 months and the tarot migration is running right now.
The roadmap, reconciled
Four documents read like roadmaps. They are not four plans — they are one plan at four altitudes. This is the main thing making the situation feel confusing.
| Document | What it actually is |
|---|---|
q4-growth-plan-2026-09.md | The committed bet. One question, one mechanism, one scoreboard. |
competitive-position §7 | The full sequence (Horizon 0/1/2). H1.1–1.6 is the Q4 bet, decomposed. |
FEATURE-LIST.md | The ranked forward list. Collates both repos, flags the numbers that changed. |
STRATEGY-brief §5 | The revenue-unblocking subset — effectively Horizon 0, stated separately. |
Read in that order and nothing conflicts. Use FEATURE-LIST as the working list and competitive-position §7 as the sequence.
Every item is small and sits on a verified defect. None of it is a bet.
| # | Item | Owner | Status |
|---|---|---|---|
| 0.1 | Delete the false paywall bullets — things the free tier already has | ISSUE-147 | in working tree |
| 0.2 | Raise lifetime to ~$79.99 — do not retire it. Stop discounting it. Raise annual too. 38% of units, 60% of revenue | store consoles | decision, not code |
| 0.3 | Fix two false store claims — “10 profiles free” is really 5; a 6-month plan that doesn’t exist | store-listing-copy | blocked on 3 prices |
| 0.4 | Link the store (1 link, 0.2% reach) and give reports a route (no route at all, 7.8% reach, plateaued) | ISSUE-76 | highest value here |
| 0.5 | Decide the language gate before localised reports ship ungated | FEATURE-LIST §C | window closing — see §6 |
The last price rise already ran the experiment. Lifetime went $29.99 → $49.99 in Oct 2025. Same month year-over-year, which controls for the Nov–May slump that hit every SKU:
| Jun–Aug | units | realized price | revenue |
|---|---|---|---|
| 2025 | 637 | $27.24 | $17,355 |
| 2026 | 598 | $48.55 | $29,062 |
| change | −6% | +78% | +68% |
Keep vs retire is settled by arithmetic, not elasticity. Retiring only pays if enough lifetime buyers switch to annual. Break-even switch rate = lifetime net ÷ annual net over its life (annual $39.99 × renewal depth × 0.798 payout):
| lifetime price | annual ×1.5 | ×2.04 | ×3.0 | ×4.0 |
|---|---|---|---|---|
| $49.99 | 83% | 61% | 42% | 31% |
| $59.99 | never | 74% | 50% | 38% |
| $79.99 | never | 98% | 67% | 50% |
| $99.99 | never | never | 83% | 63% |
The only direct evidence: the +78% rise moved 13% of lifetime-inclined buyers elsewhere — a lower bound, since a price rise is a smaller shock than removal. Raising the price is what settles the keep/retire question — at $79.99, retiring can never pay. Lifetime money also arrives today; annual money arrives over 2+ years, conditional on a renewal depth nobody has measured per SKU.
The genuinely mispriced SKU is annual, not lifetime. At $39.99 it is dominated on both sides: quarterly is $19.99/quarter = $79.96/year, and lifetime is $49.99. Annual is both the cheapest single year and cheaper than two years of quarterly. Quarterly outsells the new annual 480 : 126 — and quarterly is displayOrder: 1, shown first. Raise annual and reorder before adding any new SKU. Do not add a fourth subscription — the ladder is incoherent and a new rung makes it worse.
Verified: products_config.json carries no prices; there are no hardcoded prices in any .dart/.ts/.tsx; and the 30% discount works by swapping to a separate product id, not a coupon. So changing two price fields is sufficient.
| # | Console | full_purchase2025 | discount_full_purchase2025 |
|---|---|---|---|
| 1 | Google Play | $49.99 → $79.99 | → $55.99 |
| 2 | App Store Connect | $49.99 → $79.99 | → $55.99 |
| 3 | Stripe (web — easily missed) | $49.99 → $79.99 | $34.95 → $55.99 |
⚠ The trap. A Stripe Price amount cannot be edited — you make a new Price and repoint the link. /payments/get lists every active link with no dedupe by shortcode, so leaving the old one active shows two lifetime products at two prices. Deactivate it in the same change.
⚠ The analysis tooling goes quietly wrong. _payments.js’s usdPriceByType() medians price across all history per SKU. After this, full_purchase2025 holds both $49.99 and $79.99 and its median drifts for months — every per-SKU revenue figure stays plausible and becomes wrong. Patch it to bucket by (SKU, month) before changing prices.
Decide what the discounted price really is. Every website CTA now routes through applyDiscount?code=WEBPROMO1, which serves the discount SKU — so web-acquired buyers pay $55.99, not $79.99. That is +12%, not +60%. Today that path is tiny (21 of 1,551 purchases), but the CTA fix was built to grow it. $55.99 is the price most new web users will actually see — choose it deliberately.
Why 0.4 matters more than it looks: every conversion baseline measured before 2026-09-07 was measured against a funnel whose exit did not exist — all 12 upgrade gates dead-ended on an unregistered /premium route. Do not scope pricing against those numbers.
The loop. Existing user adds a partner → gets a live, current-state relationship reading → sends it → partner opens it with no install required → partner sees content about themselves → creates an account to unlock their side. Deliberately not gated: the first user gets full value alone, which isolates the variable being tested.
The test is not engagement and not shares. It is new users who appeared because existing users brought them.
| # | Item | Note |
|---|---|---|
| 1.1 | Relationship transits — give the relationship matrix the all transits/weekly/monthly lenses the self-matrix already has | A prerequisite, not a parallel idea. Three of the Q4 artifact’s four lines are this. ⚠ The docs call this “not an engine build” — that is wrong, see below |
| 1.2 | A one-word verdict on every pair, 12 facets underneath | We compute more and conclude less; a user can’t retell what the app said |
| 1.3 | Relationship types — friend, family, colleague | 9 of 12 facets are already non-romantic. Re-labelling, not new astrology. Ship after the bet |
| 1.4 | Invite-from-chart flow | 155,361 accounts already hold another person’s chart |
| 1.5 | Shareable pair card — “Us”, not “me” | The competitors’ actual unit of growth |
| 1.6 | Fix the cold-arrival landing first | 93.2% of share-link arrivals bounce under 10s. Do this before 1.5 sends more traffic at it |
| 1.9 | Surface the comparison to the 72.2% who never make one | O(n²) — 79,800 pairs at 400 charts. Pre-filter and cap |
| 1.13 | Per-user event measurement | See §5. Gates grading the entire quarter |
Both FEATURE-LIST §8b.1 and competitive-position 1.1 claim the engine already supports this — “it treats the composite as a natal chart” and “its orb system has a transit slot for synastry/composite.” Both supporting points are misreadings. Verified in the engine source:
CalcChart()has exactly two chart slots —dPosandTransitPos. A chart is natal (syn=0), synastry (syn=1), or transit (syn=2).applyComposite()consumes both slots to build the composite — it midpoints person 1 and person 2 intodPos. Line 869 only labels the result like a natal chart.- Transits to a composite need three charts at once — person 1, person 2, and the sky now. The two-slot model can’t express that in one call.
Orbs[]is[0]=natal, [1]=synastry/composite, [2]=transit— three orb settings for three chart types, not a transit slot for composite.
Three options, not equivalent. (1) Transits to the composite — what the plan implies; needs a third slot or a two-pass API. Real engine work. (2) Davison chart — the midpoint in time and space, so it has a genuine date/time/location and runs through the existing engine as an ordinary natal chart with syn=2 transits, no engine change. Not implemented anywhere. (3) Each partner’s own transits side by side — works today with two existing calls, but answers a different question.
Name which one 1.1 is before estimating it. The asymmetry itself is real: the self matrix has six lenses, the relationship matrix has none.
Instrumentation is a prerequisite, not a follow-on. The Q4 plan says so explicitly, because shipping the loop without it reproduces the exact failure the whole strategy diagnosed: real work shipped with no way to know if it moved anything.
- The Coach / Oracle session layer — full spec already written
- The Oracle as a shareable weekly card
- Practitioner marketplace — 4,011 Astrologers at 54.24% conversion; AstroTalk is a $43M comparable, and it’s built on an asset competitors can’t replicate
- Audio — the stack is shipped; contingent on a hire
The twelve-month concentration four systems, not twenty features
Matrix
The first-level answer everywhere. Raw astrology becomes evidence, not interface.
Relationships
Persistent people, not synastry inputs.
Timeline
The only one of the four with no roadmap entry anywhere.
Memory
~4.5M user-written questions we already hold. The pillar that cannot be bought.
AI sits across these — it is not one of them.
And a fifth thing not to postpone: surfacing. The product is already large. The next win is not building more — it is making users realise what they already have.
Explicitly deprioritized this quarter
More chart types · generic interpretations · broad AI-chat · journaling and meditation expansion · a generic social feed · large UI redesign · extensive ASO iteration · growth-content campaigns · offline/local calculation (real, but a practitioner monetisation play, not this quarter).
Numbers — quotable, and retracted
Before quoting any level, run node scripts/_analytics.js verify — about 2.5 minutes against live production. Owner: FINDINGS-2026-09-11.md.
Verified safe to quote
| Accounts | 460,694 · 5.37% ever paid · 3.73% paying now |
| The one axis that explains revenue | Charts held. 1 chart → 1.58% · 2–3 → 5.92% · 4–7 → 13.94% · 8–15 → 34.29% · 16+ → 54.24%. A 34× monotonic spread; behavioural segment collapses to ~1.00× once charts held is controlled for |
| The two opportunities | ~11,500 accounts hold 8+ charts and never paid — a conversion play. And 300,710 Solo accounts at 1.58%, where the 1→2 chart step is the real activation event |
| Scale | 87,467 MAU (Android 43,983 / iOS 43,128) · $0.20 ARPMAU · ≈$17,195/mo · ads ≈29% of revenue |
| The decline | Android active device installs 137,256 → 88,659 (Dec 2022 → Aug 2026). Steady, not accelerating. Android-only — no verified iOS equivalent exists |
| Blended LTV, all new accounts | ≈$0.85 per new user ($0.50 IAP + ~$0.32 ads). Correct for what it measures — a blended average over overwhelmingly organic installs. ⚠ Not the paid-ads bar |
| Paid-ads bar | ≥ $1.78 net per install, IAP only — measured on the acquired PRIMO10 cohort (643 installs, 46 payments), which converts at 7.15% vs 1.6–1.8% blended. Acquired users convert ~4× better because they clicked an astrology ad on purpose. Before price work-through, renewals or ad revenue. Run scripts/_ads-roi.js, which recomputes it from the ledger every run |
| Store payout | 79.8%, not 70% |
| Payer value is rising | avg net $17.34 → $25.10 (+44.7%) as the $49.99 and quarterly SKUs displace legacy ones |
| Age pays | A 65+ user is worth 8.8× an 18–24 user. Ages 45+ are 18.9% of accounts and 32.2% of revenue |
| Tarot | 6.26M journal entries, 4.54M carry a question · ~638k completed readings/yr at 67% completion · 8.5% of free-text questions are romance |
| Discoverability | reports 7.8% reach (no route at all) · harmonizer 2.8% (1 link) · store 0.2% (1 link) |
| Relationships | 155,361 accounts (34.6%) hold another person’s chart · 72.2% of multi-chart accounts never opened the relationship screen · 93.2% of share-link arrivals bounce under 10s |
| Website | ~98K monthly users · CTA click rate ~3.2% |
Retracted do not quote these again
| Wrong | Actual |
|---|---|
| 15.02% ever paid · 448,892 accounts | 5.37% · 460,694 |
| Solo 35,430 @ 4.69% | 300,710 @ 1.58% |
| “Tarot converts worst” | False — an aggregation artifact. Tarot converts better at every collection size |
| Practitioner segment “63% converted, an ARPU play” | ~11,500 unconverted — a conversion play |
| Synastry ~23% of chart views | 6.5% |
| “316 documents carry user text” | 4,544,139 |
| Website CTA rate 0.21% | ~3.2% |
| “$585K/yr” revenue | ~$15.5K/mo new + renewals ≈ $20K/mo |
| GA4’s “50% crash in H2 2024” | Measurement artifact — use Play Console device counts |
| Chani’s “42% retention / 3.1× premium” | Fabricated. Never cite |
| “There is no Meta pixel on the site” | It exists — it was firing into an orphaned pixel |
| Any chart-count threshold above 5 | Mechanical, not predictive — the free limit is 5 charts |
The standing rule behind most of these: a sample covering 16.6% of accounts was quoted as fleet levels. It inflates conversion ~2.8× and undercounts volume 14–44×. Relative lifts in those docs still hold — only levels moved.
The one thing blocking everything
Per-user event measurement does not exist — ISSUE-77, ISSUE-142.
Metrics go to Prometheus, an aggregate time-series store. A uid label across 460,694 accounts is a cardinality explosion, not a one-liner. This was mis-priced as trivial and it is design work — a uid-keyed event collection that does not exist yet.
What it blocks: grading the Q4 bet · every gating, capping and pricing question · per-session funnels · experiment assignment.
app_version and platform now tag every metric at the single choke point every metric passes through. So “did release X improve anything?” is now answerable.
“Did this session go A → B?” is not, and cannot be without an event store. That is the honest boundary.
Live state — verified today
The fire docs’ deploy-state claims lag reality in both directions. These three were checked against git directly on 2026-09-11.
The fire doc ranks this “TOP PRIORITY, committed but not pushed.” It is in origin/master (e90aa02). Pushed still isn’t deployed on the Node box — but the doc’s framing is stale and shouldn’t drive the priority order.
12b9965 is in origin/master, not pending. FEATURE-LIST §C says to decide the language gate “before this is pushed” — that window has closed or is closing. Confirm whether it is deployed, then treat Horizon 0.5 as a live decision rather than a future one.
In the legacy astromatrix repo, local master is 8 ahead / 2 behind, and four of those commits are reverts. The ISSUE-106 GDPR tarot-deletion fix, the ISSUE-107 fix and the ISSUE-82 Sabian fix were each committed and then reverted.
Verified: RemoveAllData in Website/API/UserController.cs does not touch TarotReadingJournals today. The docs describe these as “fixed, committed, not pushed.” They are not in the working tree and not pushed. Whether the reverts were deliberate needs an answer before anything is re-applied — and the legacy database sunset makes it a dated question.
Uncommitted right now
| Repo | Files | What it is |
|---|---|---|
| astromatrix-public | app/home/terms/ Footer.tsx style.css | ISSUE-144 — the Terms URL printed in every store description 404s. Page restored, Footer link added. Blocks an ASO submission (Apple 3.1.2). The restored text has no auto-renewal clause — a lawyer’s call, not an audit’s |
| astromatrix-flutter new-ui | products_config.json gated_content_widget.dart | ISSUE-147 / Horizon 0.1 — the false paywall bullets, removed |
| astromatrix-nodejs | doc edits + 4 unpushed commits | ISSUE-143/144 fixes and a navigation-architecture UX audit |
- Branch note: the shipping Flutter branch is
new-ui, which is 116 commits ahead ofmaster. Don’t reason about Flutter state frommaster. - Website deploys work again. Push to
astromatrix-publicmaster→ Gitea Actions → live. Runnpm run buildlocally first and checkgit log origin/master..HEAD, because the server takes whatever is onorigin/master. The “no auto-deploy” note in older docs is stale. - Concurrent sessions are editing these docs.
docs/README.mdchanged on disk mid-session. Re-read before editing anything underastromatrix-nodejs/docs/.
The doc map
| Doc | Owns | Trust |
|---|---|---|
| FINDINGS-2026-09-11.md | Verified fleet numbers and the retraction list | read first |
| product/FEATURE-LIST.md | The forward list — what to build next | working list |
| product/README.md | Index for the product-design lane | current |
| docs/README.md | Index for the support / fire / migration lane | current |
| q4-growth-plan-2026-09.md | The committed quarterly bet | current |
| competitive-position-2026-09.md | Market position, Horizon 0/1/2, the AI-era answer, Memory as the fourth pillar | newest strategy |
| strategy-reference-2026-09.md | Ranked strategy map, visibility map, turnaround research, what NOT to do | levels predate 09-07 |
| built-but-disconnected-2026-09.md | Four assets built and unshipped: sky events, broadcast push, Big Three rarity, astrocartography | code-verified |
| acquisition-channels-2026-09.md | Creator seeding, PR, store editorial, the paid-UA math | no first-party data |
| STRATEGY-brief-2026-09-07.md | Product-consequence view of the active fires | current |
| SUPPORT-fire-manager-report | Master bug tracker — all ~147 numbered ISSUEs | authoritative |
| SUPPORT-fire-FLUTTER / NODE / PROCESS | Derived per-area dev specs | derived; FLUTTER self-flags stale |
| SUPPORT-ux-experience-issues.md | Non-technical UX complaints, counted by headcount | current |
| product/five-user-modes.md | Mode briefing pack, code-verified | levels superseded |
| product/app-map-and-changes.md | Verified app map — check here before claiming a screen is unreachable | current |
| user-knowledge-audit-2026-09.md | What the product knows about a user; the proposed knowledge model | new, untracked in git |
| INFRA-resilience-roadmap.md | DR / failover phases, open security items | current |
| MIGRATION-*.md | Legacy .NET → Node port | 3–6 month clock |
| SESSION-HANDOFF-2026-09-11.md | Ads, revenue economics, the website funnel | read before the test plan |
| paid-ads-test-plan-2026-09-09.md | Ads working doc | 4 generations of economics, 3 wrong |
Traps
Things that keep getting re-derived wrongly. Each one has cost a session already.
- Don’t propose a feature without checking whether it already exists. Event-triggered push, UGC/community, rewarded ads, the Guide deep-link and the in-chart South Node were each proposed as new work and each already shipped. Two of two “Bet”-tier items in one doc already existed.
- Usage of an undiscoverable state is not a measure of demand.
/chartupcomingat 0.2% and Progressed at 1.3% are evidence of no route, not no demand. - Never convert a destination into a state without leaving a signpost for one release. This has produced four confirmed “you deleted it” reports for features that still ship. ~25 of ~49 UI complaints are “I can’t find things”; one named complexity.
- “Too complex” and “too sparse” are both right. Six users want more on screen; one articulate user wants less. Solve it with a density mode — never by picking a side.
- Never quote a truncated list as complete. Print a count, reconcile it, state N of N. Paginated Meta reads return partial data silently under rate limiting.
- Verify writes by re-reading state — not by an exit code or a chained
echo. profiles= accounts (531,848).users= charts/people (1.24M). Not interchangeable, and they have been mixed up in published figures.- Pausing a Meta campaign does not pause its ads. Audit
status, nevereffective_status. - Committed ≠ pushed ≠ deployed ≠ released. For Flutter, add ≠ in a build a real user has.
So: what now?
The shortest defensible answer, in order.
- Horizon 0.4 — link the store, give reports a route. Small, verified, and everything downstream is unmeasurable without it.
- Decide the lifetime SKU (0.2) and the language gate (0.5). Both are decisions rather than builds, and both are load-bearing.
- Scope the event store properly. It gates grading the quarter, and it is not the one-liner it was priced as.
- Ship Horizon 1.1 — relationship transits. It is the prerequisite half of the committed bet that the Q4 plan assumed already existed.
- Leave the ads campaign alone until day 3 gives an honest CPI against the $0.85 bar.
- Resolve the legacy .NET reverts before the database sunset makes it urgent.