Three things, one of which we were quietly getting wrong.
REAL OPT-OUT. We told members "removable any time" in three separate places and there
was no way to remove anything. Same class of failure as the dead "Add mine" button:
copy written, mechanism never built. The profile card now offers Remove username,
Remove email and Remove everything, and profiles.remove() clears the value while never
touching the position. Adding it again later works exactly as before, so opting out is
not a one-way door.
It is a two-step inline confirm, not a native confirm() dialog. Browsers with "suppress
dialogs" switched on return false, which would have made Remove look broken in precisely
the way Add mine was broken. First tap arms and explains the consequence, second tap
does it, and it disarms itself after six seconds.
THE PROMISE WE WERE BREAKING. The payout mailer and the upgrade alerts read
member-alerts.json, NOT profiles.json. So a member who completed the new profile got
NOTHING, while the invitation card promised "a note the moment POL lands in your
wallet". Verifying a profile email now mirrors into member-alerts.json so every existing
alert path works, including the unsubscribe link, and removing the email clears both
stores so opting out actually stops the email.
MANSON'S HUGE ASTERISK. He asked for it to be bigger and bolder so nobody can say they
did not see it, and on a decentralized build that burden is ours, not the member's. One
gold badge now appears on the dashboard invitation, inside the dialog on every step, in
the inbox banner and on the profile card itself: "100% OPTIONAL - never required", with
the plain statement that the position, the payouts, the team and everything on the page
work exactly the same without it, nothing on chain depends on it, and it can be removed
again any time.
gate-e2e is 52 assertions, up from 38. The new ones prove one tap does NOT remove
anything, the second tap does, the server agrees the value is gone, an email-only
removal leaves the username alone, and the invitation reappears afterwards so the whole
thing is reversible. profiles-unit 28, signin-fallback 7, captions-e2e 158 all green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three things.
GERMAN AND PORTUGUESE. Marty's call, and the right one: the translation cache only
shows who is already here, not who Manson is bringing. Manson knows his audience, so
Portuguese goes in even though site data ranks German above it. All five videos now
carry English, Italian, French, Spanish, German and Portuguese. Disclaimers checked by
hand in the two new languages: "Es wird kein Einkommen garantiert" and "Nenhuma renda
e garantida".
FINDABILITY. Marty went looking for the multilingual feature on his own training page
and could not find it, because the first build leaned on the native CC control and
nothing switches on for an English reader. A feature nobody can find is not shipped.
Every captioned player now carries a visible row of subtitle chips underneath, named
in their own language, with an Off option. Captions still stay off by default for an
English reader and still turn themselves on for anyone who already picked a language
with the globe button; the chips just make the choice visible instead of buried.
THE DEAD BUTTON. The inbox banner's "Add mine" shipped with NO click handler, so it
rendered and did absolutely nothing. That is why Marty could not add his address while
connected to the right wallet: nothing was listening. It now opens the same optional
profile dialog and re-renders when it completes.
Tests: gate-e2e 38 (up from 33, and rendering that button is explicitly not the test
any more, it has to actually open the dialog), captions-e2e 158 across six languages
including the new chip behaviour, clicking one and turning them back off.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Manson's objection was that requiring a username and a verified email pulls the
build back toward a centralized database of members. He is right, and the
communication gap is real too, so the answer is to ask well rather than to force.
Nothing about holding a position, getting paid, reading the org, the training or
the tools depends on contact details any more. There is no onboarding gate: a
brand-new member registers, lands on their page and is never stopped by a modal.
The dashboard offers a dismissable card ("Not now" snoozes it for a week) that
leads with the thing members actually want, a note the moment a payout lands in
their wallet, and says outright that everything works the same without it. The
inbox is the one place that asks, because a message cannot be delivered to
someone who left no way to reach them, and even there it is an invitation.
The card sits above the tab strip rather than inside the dashboard pane: the page
opens on the pitch tab, so an invitation parked in the dashboard would never be
seen by the new members it is aimed at.
For leaders, /api/public/reach answers "how many of my org can I reach off the
site", scoped by chain.isInTeam so it leaks nothing upward or sideways. That
makes coverage a leader's own problem to solve by asking, not a rule imposed on
members.
Fixes a real bug found by the rewritten suite: the dismissable flag double-booked
as "single-field edit", so saving a username in the opt-in flow closed the dialog
instead of advancing to the email step. Split into oneShot; the suite now asserts
the advance as a regression.
QA, all green: profiles-unit 28, signin-fallback 7, gate-e2e 33 (rewritten to
assert the opposite of what it used to: no forced modal, dismissable everywhere,
visitors unaffected), join-flow 12 cold / 11 refuse / 12 warm.
qa/reseed.sh carries two hard-won guards: never name a shell variable TMP on
Windows (it inherits the system temp dir and rm -rf wipes it), and never pkill.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first-time gate cannot be dismissed; editing later can. The 'Email me my alerts' card becomes
'Your member profile' for a signed-in owner, showing the username and confirmed email with Change
buttons, so the verified address is the one source of truth for both messaging and payout alerts.
Visitors and non-owners keep the original opt-in form unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Marty, 2026-09-16: leaders can write but 94% of positions cannot receive (47 of 771 have ever signed
in to messaging, 482 messages sit 85% unread). profiles.js stores username + verified email per
POSITION (one wallet holds one position, so a Triple Play holder has three; the person is the email
and one email may hold several positions). Seeds the 40 emails already on file from
member-alerts.json, pre-filled but unverified so confirming costs one tap.
Writes are only ever accepted from a session that PROVED ownership: wallet personal_sign
(messages.verifyChallenge) or the Telegram Mini App bridge. The public /my/<id> page is untouched and
cannot write a profile, verified by test: all four endpoints 401 unauthenticated while /my/21 stays
200. Endpoints GET /api/public/profile, POST .../username, .../email-start, .../email-verify, plus
GET /api/admin/profiles for coverage. Email codes: 6 digits, 15 min, 60s cooldown, 5/day, 6 tries.
profile-gate.js is a two-step modal that cannot be dismissed, fired on dashboard boot (covers the
Mini App landing) and right after a wallet sign-in. Chatbot canned answer + AI prompt updated.
28 unit tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The team pyramid overflows on phones. Add -/+/Fit zoom controls to the matrix
(pyramid) view that scale it via CSS zoom, defaulting to auto-fit so the whole
structure fits the screen width on load. Controls show only in pyramid mode,
re-fit on resize and when the dashboard tab opens, and guard against measuring
while the tab is hidden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Matches _payUpline (level > levelIndex, levelIndex = buyer.level-1). The old
depth+1 rule told #21 (Culmen) it could not catch #49's Apex buy; #21 is #49's
4th matrix upline and does catch it. Fixed in the owner alert, coaching scan,
dashboard pipeline, AI prompt, leader digest, chatbot and how-pay-works copy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A member sent a nudge to #102, then went looking for what had gone out. The
only feedback was a small "Delivered to their dashboard" note, so she checked
her clipboard - which still held the unrelated "Copy plain-English summary"
text describing her OWN position. She reasonably concluded the nudge had sent
her position instead of the recipient's.
It had not: the message log shows #34 -> #102 carrying #102's own numbers.
The feature was right and the feedback was wrong, which is its own kind of
bug - a member who cannot verify what they sent will assume the worst.
Every nudge now has a Preview button that names the recipient and shows the
exact text before sending, and on success the panel shows what was sent and
to whom rather than a bare "Delivered".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The upgrade panel showed a price, a funded badge and a button. Nothing on it
said what the member would get. 53 of 87 qualified members in the org are
parked at Ascensus, and a panel that reads as a bill is part of why.
It now leads with what the rung unlocks, in the member's own numbers: the
generation it extends reach to, how many people they already have sitting
there, and what each of those pays when they climb. genCounts already existed
in getOrgShare; this just points it at the member instead of at the admin.
The line that matters most is arithmetic, not persuasion: because every rung
costs exactly twice the last, the first catch at the new level is exactly
double what the upgrade cost. That is Lesson 8's whole argument, and the
panel now makes it where the decision is actually taken rather than leaving
it in a lesson nobody opens at the right moment.
Wrapped in try/catch — if the org walk fails the panel renders as before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Janie linked successfully and the dashboard kept showing the Connect button,
so a working link looked like a failed one — and she had no way to tell which.
The Messages panel had no idea about link state: msg-inbox never reported it.
It now returns tgLinked (from tgbot.memberChat) on the response the panel
already fetches, so this costs no extra request. Linked members get a
confirmation telling them where their pings arrive and how to open the Mini
App, instead of a button asking them to do something already done.
Third variant today of the same failure: the interface not knowing a state it
could easily have known, and the member left guessing. The bot had it too —
telling an already-linked member their code had expired.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Janie can now sign in from Trust's DApp browser but cannot link Telegram.
Cause: the handler called window.open(url,'_blank'), and wallet DApp browsers
routinely block programmatic window.open. The call does nothing, no error is
raised, and the member is left staring at an unchanged screen.
Now the button fetches the link and then renders a real anchor for the member
to tap — an actual tap is handled by the OS, which is what launches the
Telegram app — plus a "copy the link" button with an execCommand fallback for
webviews that refuse both.
Third failure today from the same root assumption: that members are in a
normal browser. They are usually in someone's webview.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Janie's screenshot showed the X / chevron / share / kebab chrome of TELEGRAM'S
in-app browser, not Trust's DApp browser. She tapped her sponsor's link in a
Telegram chat, Telegram opened it in its own webview, and that webview can
never hold a wallet. The page said "no wallet in this browser", she said she
was using Trust — both true, completely at cross purposes.
Our advice made it worse: "open your wallet's browser and type the address" is
useless to someone whose whole reason for being on the page is that they
tapped a link.
Now we name the browser they are actually in — Telegram, Facebook, Instagram,
LINE, Messenger, X, or a generic Android WebView — say plainly that it cannot
hold a wallet and that this is not their fault, and give them the one useful
thing: a button that copies the address to the clipboard so they can paste it
into Trust. There is an execCommand fallback because older webviews have no
clipboard API, and leaving a button that silently does nothing would be worse
than not having one.
Detection deliberately requires the absence of window.ethereum before calling
something an in-app browser: wallet browsers ARE WebViews, and those are
exactly the ones that work. Verified against Telegram, Android WebView, Trust
(correctly ignored) and mobile Safari (correctly ignored).
This will hit far more members than Janie — every link tapped inside a chat
app lands in the same trap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two things Marty raised.
1. Member pages did not look like the rest of the site. The byline is now the
gold uppercase eyebrow used above every other h1, the heading is larger and
tighter with the last beat in gold (house style is "Team <gold>Grants</gold>"),
and the subhead steps up from 18px to a fluid 19-22px in a brighter colour.
The headline is model-written so the gold split cannot be hand-picked —
goldTail() takes the final two words, or one on a short line. Everything is
escaped before the span is added; the only markup is ours.
2. The "no wallet found" error listed five wallet names and described a DApp
tab without saying the thing that matters: you are in the wrong browser,
and here is the address to type. It now says exactly that, names the page
address, and warns that Trust on iPhone has no browser at all any more —
previously those members were reading instructions they could not follow.
Diagnosed from a real case (#34): the company dApp at thermcircle.com supports
WalletConnect, so it connects from any browser; this site is injected-provider
only, so it needs the wallet's own browser. That is the whole difference, and
WalletConnect is the actual fix — this is the stopgap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phantom exposes its EVM provider at window.phantom.ethereum and does not always
fire EIP-6963, so the picker never saw it. probeInjected() now folds in Phantom
and any window.ethereum.providers[] entries (deduped by identity + name) for
both pick() and list(). Phantom added to the mobile in-app-browser hint and the
dashboard no-wallet message.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mobile users on SafePal/Coinbase/Rabby had no route in — the deep-link help
only offered MetaMask + Trust. Added a wallet-agnostic instruction (open your
wallet app -> Browser/DApp tab -> type the join URL) that works via the
injected provider our EIP-6963 picker already resolves; join-now fills the
sponsor path into the hint. Dashboard no-wallet message made wallet-agnostic
and names SafePal. No guessed SafePal deep-link scheme (unverified).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each coach card now carries a pre-written, situation-specific teach-forward
message (all four recommendation kinds) sent over the existing wallet-verified
Messages rails: msg-send plus the Telegram bridge. When not signed in, an
inline prompt appears and the page smooth-scrolls to the Messages card.
Chatbot canned answer and AI prompt synced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New tgbot.js (raw Bot API, zero deps, private-chats only so the
group feeds are untouched): wallet-verified linking via dashboard
deep-link codes, personal payout + joined-on-your-link DMs, a
message bridge that delivers site messages natively in Telegram
with reply-to-answer routing (matrix-line permissions enforced by
the existing messages.js rules), and links/msg/help commands.
Webhook self-registers; secret derived from the token. Config key
companionBotToken overrides the feed bot token when Marty creates
a dedicated bot. Dashboard Messages panel gains Connect Telegram;
chatbot + AI prompt synced.
- Modules 2 (Help Your Two) and 3 (Teach the Teachers) live on the
training page: six Marty-voiced lessons, coming-soon card retired,
anchor ids on all ten lesson cards.
- Coach panel and pipeline now prescribe lessons per situation
(L2/L3/L7/L8 chips) - Lesson 9's routing table as buttons.
- Chatbot + AI prompt know the full ten-lesson course and the
situation-to-lesson routing rule, plus the business-in-a-box
product answer.
- New printable: /weekly-rhythm (Lesson 10's 20-minute routine +
4-week tracker, personalized QR), cross-linked from dashboard and
fast-start.
- "Business in a box" section on the join page and home page names
the full toolkit: course, promo arsenal, printables, AI coach,
live dashboard, public engine.
/fast-start: one-page checklist that personalizes with the member's
ID, invite link, and a scannable QR of /join/<id> when opened from
their dashboard (or with ?id=). Print stylesheet flips it to clean
black-on-white letter format; floating translate/chat widgets are
now excluded from printing on every page. Dashboard share card
links to it; chatbot + AI prompt synced.
Server keeps an hourly-cached POL/USD spot price (CoinGecko, 8s
timeout, last-known fallback) and rides it on the member API. The
org bar adds a dollar tile with the rate it used; hidden when no
price is known. Display-only - never used in on-chain math.
New org bar at the top of the Your team panel: total members below
(full-depth rollup), generations deep, qualified count, and POL
earned below. Chatbot canned answer + AI prompt updated in sync.
A member below you pausing on an upgrade never blocks money - payments
pass UP through them to the next eligible position and everything
below keeps flowing. Bake that explanation into: the dashboard
pipeline panel (micro-note when building rows exist), the chatbot
(canned answer + AI system-prompt objection entry), and the coaching
skill docs.
- messages.js: challenge/personal_sign/recover auth (vendored pinned
js-sha3 0.9.3 + noble-secp256k1 1.7.1, server-side only; self-tested
positive + tamper cases), 30d HttpOnly sessions, message store on the
volume, matrix-line permissions (your downline direct or broadcast, your
upline chain - nothing else, so spam is impossible by construction),
daily rate limits (30 direct / 3 broadcasts), 1500-char plain text
- chain.js: memberIdByAccount (wallet -> position for sign-in)
- API: msg-challenge/-verify/-me/-inbox/-send/-read public + msg-unread
(count only, no auth) + admin/messages (full visibility, disclosed to
members in the UI)
- Dashboard: Messages card with unread bell, one-tap wallet sign-in,
inbox with auto-read, compose with to-ID or whole-team broadcast
- Admin: Member Messages review table
- Chatbot canned answer + AI system prompt updated
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every coaching output now ends by teaching the recipient how to teach:
point them at the Coach Your Team panel on THEIR own dashboard and tell
them to run the same play for their two.
- Member dashboard dCoach footer: "you're not coaching two people - you're
teaching two coaches"; easy-win rows add "show them how you spotted it"
- Admin Coaching Radar: teach-forward rule footer
- AI chat system prompt: COACHING DOCTRINE section so chat answers frame
guidance teach-forward
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- chain.js getCoachingScan(rootId): classifies everyone below a root into
atRisk (POL forming they can't catch - corrected bought-level rule),
rollForward (qualified-at-Scintilla with entry rewards covering Ascensus),
and oneAway (1/2 directs)
- Admin: GET /api/admin/coaching?root= (name-decorated) + "Coaching Radar"
panel with tiered who/what-to-say/POL-at-stake rows, auto-loaded
- Member dashboards: memberPublic now returns .coach scoped to the member's
own leg; new "Coach your team" card shows the same triage so every member
coaches their own team - computed live, no snapshots or cron needed
- Chatbot canned answer + AI system prompt updated to describe the panel
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The pipeline widget, owner upgrade alerts, and the org routing/leak map all
treated a position as eligible to catch a payment at the buyer's PRE-upgrade
level (level >= depth). Verified against on-chain history: position 6, at
exactly the pre-upgrade level (Ascensus) and qualified, was passed over on
position 8's Fabrica buy - so the contract requires the catcher to be at the
level being BOUGHT (depth+1), matching the "stay one level ahead" doctrine.
- chain.js getOwnerUpgradeNeeds: eligible now pLevel >= depth+1; neededLevel
reported as the bought level (was one low, so alerts would have stayed
silent in exactly the situation they exist for)
- chain.js getOrgRouting catcher(): same correction in the routing simulation
- my.js pipeline: needLevel/eligible/summary-text ladder corrected; warnings
now name the right level
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- chain.js getOrgShare now returns genCounts (breadth-first members per
generation below the root)
- Admin "Your Organization vs. the Network" panel renders a Gen 1..N bar list
with fill-vs-capacity (2^n slots)
- Member dashboard "Your team" card gains a Team Depth section computed from
the subtree client-side, labeling each generation with the level whose
upgrade pays that position (gen D pays at the level D+1 buy)
- AI chat system prompt updated to describe the new dashboard section
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/join-now now honors ?ref=<id> (resolves that member's moving-link joinTarget;
falls back to the global rotation sponsor). Repointed: /start 'Join With Current
Sponsor' -> /join-now; /my pitch buttons -> /join-now?ref=<id>; invite page
/join/<id> join button -> /join-now?ref=<id>; bare /join redirect -> /join-now.
'Get Started' learn CTAs still go to /start for onboarding. When the flag is off,
/join-now gracefully redirects to /start, so the wiring is safe either way.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- default tab now: shared /my/<id> link -> Team Build pitch; form lookup /
returning member on bare /my / #dash link -> dashboard (replaces the flaky
"have you seen this ID before" heuristic that dumped returning prospects
straight into the dashboard).
- added a big gold "Join the Team" CTA in the hero (right column), above the
fold, so a sold visitor can join without scrolling past the video.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turns the member page into a persistent, tabbed dual-purpose page:
- Persistent context strip: "Member #X's RM Circle page · Verified · Qualified"
- Tab 1 "The Team Build": pitch for promoted-link visitors — hook, embedded
overview video, 3 steps, live proof (member's real on-chain numbers), Join
under #X CTA. Persistent (replaces the dismissible cold-intro banner).
- Tab 2 "Position Dashboard": the full dashboard as-is (facts, plan, matrix,
pipeline, alerts, share, payments, lineage).
- Default: cold visitor opens on the pitch, the member opens on their dashboard;
both always one tap apart. Retired the coldIntro banner + its handler.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The dashboard serves two audiences — the member and prospects who click a
promoted link. Expanded the cold-visitor intro into a proper "New here? Start
here" panel at the top: what it is + 3 get-started steps + CTAs (join under this
member / watch training / strategy), and reframes the member detail below as
live proof. Member's own view still hides it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a synthesized next-step panel to /my: the level ladder (you-are-here →
upgrade-next), the single clearest action (qualify vs upgrade), and a funded
badge that checks the member's wallet against their next upgrade cost.
Privacy: the wallet balance is checked server-side (chain.balanceOf via
eth_getBalance) and ONLY a funded true/false is returned — the raw balance is
never exposed on the ID-addressable dashboard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Domain migration go-forward: replaced every hardcoded rmcircle.saasy.top
across public/*.html (canonical, og:url, og:image, twitter tags),
chat.js (chatbot answer URLs + linkify self-link matcher), my.js, and
server.js (member-page OG injection, unsubUrl, chat system prompt) with
rmcircle.team. Old domain still serves everything; canonical now points
search engines + social previews at the new primary domain.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- /disclaimer: independent-resource, affiliate, earnings, risk, and
not-advice disclosures; linked from all footers.
- Self-service alerts: members opt in with their email on /my/:id to get
"you've been paid" + "upgrade needed" emails for their own position
(same watcher as the owner alerts, extended). Signed unsubscribe link
(/unsubscribe?id=&t=HMAC), on-chain-registration check, rate-limited,
masked-email status, confirmation email.
- Admin login: timing-safe compare + per-IP lockout (8 fails -> 15 min,
escalating). Previously unlimited/brute-forceable.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generates a chat-ready, per-member explainer of the upgrade/generation
payment structure — the recurring "will I miss upgrades?" question
answered from that member's real position: their spot, the one rule
(each layer pays you once at one level), their ladder with amounts and
which layers they're already eligible for, an honest "you miss it only
if you fall behind" note, and a calibrated next move (no false urgency —
distinguishes a layer that's close from one still climbing). One click
copies it for pasting into Telegram/chat; works for any member by ID.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Makes the depth-matched payment routing visible per member: walks their
subtree and shows who is ONE upgrade away from paying them (member at
depth D whose level equals D — their next upgrade routes to this
position first), the exact amounts from the contract's cost tables
(exposed via the member API), eligibility warnings (needs 2 directs /
needs level X), a ready-now total, and whether it covers their own next
upgrade. Members building toward their pay-you level are counted, with
the takeaway line "helping your leg upgrade IS your income."
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/my/<id> links circulate (address-bar shares, pre-/join links), and a
cold prospect landing there saw stats with no story. First-time viewers
of a position now get a banner: two-sentence RM Circle summary, a
"Read your invitation from this member" CTA into /join/<id>, a strategy
link, and the risk line. Hidden for repeat viewers of the same position
(likely the member) and dismissible per session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>