Manson narrowed the ask to five: how the team build works, joining on the site,
joining in the dApp, your level is your reach, and the textbook play. Those five now
carry English, Italian, French and Spanish caption tracks. The other fifteen training
videos are deliberately untouched.
Languages are the top three by real demand from the translation cache rather than by
instinct. German still outranks Portuguese there by more than double, which is worth
settling before adding a fourth.
Captions stay off for an English reader and switch on automatically for anyone who
already picked a language with the globe button, so this rides the choice members
have made rather than adding a second one.
Disclaimer language came through intact in all three, checked by hand because a
softened "no income is guaranteed" is a compliance problem rather than a typo:
Nessun reddito e garantito / Aucun revenu n'est garanti / No se garantizan ingresos.
A native-speaker read of those specific lines is still worth having before this is
promoted anywhere.
qa/captions-e2e.mjs now discovers the captioned players from training.html instead of
a hardcoded list, so it cannot drift as videos are added, and it reads the parsed cues
back out of each player: 91 assertions covering content type, cue counts, the right
track showing per language, English never showing alongside, and a guard against a
track that is really English wearing a foreign label.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Manson asked whether the training videos could be in other languages. A real dub
means re-rendering every video per language: translated Romance-language speech runs
15-25% longer than English and these are slide videos with fixed beat timings, so
swapped audio drifts off what is on screen. That turns 15 files into 75 and makes
every future lesson edit a five-way job. Captions keep ONE video and add small text
tracks beside it, so editing a lesson re-captions that lesson only.
tools/captions.mjs: ffmpeg pulls the audio, ElevenLabs Scribe transcribes with word
timings (the same STT we already use to verify voiceovers), words are grouped into
SENTENCES, and only then translated. Translating cue-by-cue is why most auto-captions
read badly at cue boundaries. Translation goes through our own /api/public/translate,
so every phrase caches forever in translations.json and costs once across the site.
The STT response is cached on disk because it costs money; never pay for it twice.
Language choice is evidence, not instinct. The translation cache shows real member
demand: Italian and French far ahead, then Spanish, then GERMAN - which beats
Portuguese by more than double, the opposite of what we assumed. Proof of concept is
Italian on the 5-minute overview.
Captions are deliberately NOT on by default. An English reader does not want them
forced over the picture; someone who already switched the site to Italian almost
certainly does. public/vtt-lang.js shows the track matching their 🌐 choice and
leaves the player's CC button to do the rest.
Two bugs this caught in my own code, both found by reading the output:
- the line wrapper truncated each cue to two lines and SILENTLY DELETED the overflow,
so "the whole plan fits in one sentence" shipped as "fits in" then "sentence". It
now chunks by the real wrapped line count and never drops a word.
- a one or two word tail ("sentence." alone on screen) folds back into the previous
cue.
The suite asserts all 780 transcript words survive into the English track.
Also: .vtt had no Content-Type mapping, so it served as octet-stream and browsers
silently ignore such a track. qa/captions-e2e.mjs (12 assertions) reads the parsed
cues back out of the player rather than trusting the markup, which is the only way to
catch that class of failure. profiles-unit 28 and gate-e2e 33 still green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
qa/live-visitor.mjs is read-only against production: a visitor on any ID-keyed
shared link must see no modal, no invitation, no 401 and no page errors. It is the
check that the optional-profile change did not disturb anyone who is not signed in.
Passing 9/9 against rmcircle.team.
docs/for-manson-optional-profiles.md is the note to send him before anything goes
to the team, since he asked to see it first. It leads with his objection being
right, states plainly that nothing is required, and is honest about the trade: we
accept lower coverage rather than mandate identity. It also covers the level-8
question, where the pay rules were already fine and only the ladder graphic was
misleading.
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>
qa/harness-server.js boots the real server with chain reads stubbed (COLD=1 reproduces the cached-index
state that left #787 without a profile). qa/join-flow-e2e.mjs drives the real /join-now page with a fake
wallet producing genuine secp256k1 signatures.
Three scenarios pass: signs on a cold index (9) - the gate appears on their own dashboard; refuses to
sign (10) - REGRESSION, the join still completes and redirects with the page usable; signs on a warm
index (9). Full set green: profiles-unit 28, signin-fallback 7, gate-e2e 47, join-flow 9/10/9, plus the
live shared-link check.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#787 registered at 18:35 CT, 22 minutes after the first join-flow fix, and still had no profile. Two
causes, both fixed:
1. messages.verifyChallenge resolved the wallet through chain.memberIdByAccount, which reads the
CACHED index. Seconds after a registration that wallet is not in it, so the signature was rejected
with "No RM Circle position is registered to this wallet". It now accepts an idHint (the position
id from the member's own registration receipt) and, on a cache miss, reads that id live from the
contract via chain.verifyMember, minting only when the contract says this exact wallet owns it.
That is a stronger proof than the cache, not a weaker one. Now async; the single call site awaits.
2. join-now.js fired the sign-in and a 4.5s redirect in parallel, so the page could navigate away
while the wallet was still showing the signature prompt, and it did not wait for submit-id (which
runs the live verifyMember server-side that seeds the index). It now awaits the report, passes the
receipt id, and redirects only once the signature settles, with a 120s bailout.
qa/signin-fallback.mjs (7 assertions) proves the cold-index path with real secp256k1 signatures and
covers the abuse cases: a hint for a position the wallet does not own is refused, and a signature from
another wallet is refused. Existing suites still pass: profiles-unit 28, gate-e2e 47.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by checking the live data: 59 positions registered in the 36h to 2026-09-16 and not one has a
profile row, including #785 which registered 20 minutes after the gate deployed. The join flow
redirects to /my/<newId> but the member arrives with no inbox session, so the gate never runs and the
leader still cannot reach them. join-now.js now takes the one free personal_sign immediately after the
registration confirms, while the wallet is still connected, which is the cheapest moment to ask.
Best-effort: declining does not block the join, and they are asked again on their next sign-in.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Findings fixed: the gate's buttons had no stable ids (fragile to test and maintain), and finishing the
gate left the member on the pitch tab where the profile card and Messages are not visible, so
completion now opens the Position Dashboard tab. /api/public/profile/email-start returns devCode
outside production so the flow is testable locally, matching the InstantAdPay pattern.
qa/profiles-unit.mjs (28 assertions) and qa/gate-e2e.mjs (47 assertions, real sessions, real UI) with
a README. All pass.
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>
Payout notifications migrate to the shared payments topic; registrations,
upgrades and the daily snapshot keep posting to the team-build topic.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
New config keys telegramEchoTopicId / telegramEchoChatId / telegramEchoEvents
(default: payouts). Each echoed line is prefixed with the program name because
InstantAdPay posts into the same topic.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
config.telegramProofChatId (+ optional telegramProofTopicId, telegramProofEvents
'payouts'|'all') mirrors payout events with the recruiting copy plus the
Polygonscan receipt. Inert until the chat id is set.
Co-Authored-By: Claude Fable 5.1 <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>
- Members-only: the stay-consistent video now requires a wallet-verified session
(server 403 + training-page lock UI), matching Module 3.
- Embedded on /weekly-rhythm above the sheet (hidden in print) with a members-only
fallback card for signed-out visitors.
- Added a ~31s vertical (1080x1920) social cut, ungated, for Telegram/social.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A narrated, animated companion to Circle Method Lesson 10: inspires members to
keep the weekly 20-minute rhythm, print and fill the Rhythm sheet each week, and
duplicate-and-teach rather than reinvent. Placed after Lesson 10 in Module 3,
with a watch link added to the printable /weekly-rhythm page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Weekly Huddle overlay now reads its content from /api/announce (config-
driven) instead of a baked-in block, and Admin -> Settings -> Event pop-up
exposes a toggle plus editable fields (eyebrow, flyer, join URL, date, times,
auto-hide, event id). Defaults are seeded into config on boot so the panel
reflects real state. Removed the include from join.html so it never interrupts
the join flow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Self-contained /announce.js injects a branded, dismissible event overlay
(the flyer + all four time zones + Join Google Meet button). Included on the
main funnel and member pages. Auto-expires after the huddle and shows once
per browser until dismissed; edit the EV block to reuse for future events.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
111s in Marty's voice: position #21's ledger, catch-funded climbs, the
pass-over cost, this week's Apex catch-and-climb, and the dashboard/sponsor
next-play close. Chatbot + AI prompt synced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wallet-gated like the Method lessons: the API returns titles/dates to
everyone (prospects see the shape of the product) and video URLs only with a
member session. List lives in the data volume (replays.json) so the weekly
add never needs a deploy; videos stream from the DO Spaces bucket (CSP
media-src added). Teaser on /training; chatbot + AI prompt updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The count-up theory made every fresh banner read as fully delivered, so the
sweeper deactivated brand-new campaigns minutes after launch and stamped them
complete (caught by Marty 2026-09-01 on ad 2713).
Co-Authored-By: Claude Fable 5 <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>