36 Commits

Author SHA1 Message Date
martbost 4981b1a923 Retire the rotation queue's voice; team links pick the nearest open spot in their own leg
The queue's AUTO-ADVANCE post announced a rotation event this morning when a member had
built his own leg (the Triple Play). The queue stopped placing anyone when the public link
moved to the chain walk; what was left of it was three Telegram messages, a classifier that
called any join under a former queue member a "rotation join" and swept the newcomer into
the queue, and a preference inside the team-link picker for queue positions over the nearest
open one (15 members affected).

Verified offline over the full snapshot first: all 889 personal links already target the
member or a position inside their own subtree; the walk starts at the member's own children
and cannot leave the leg. Now: the three messages are gone (counters still update silently);
a join is a rotation join only if the company link could have produced it (the company link
always offers the shallowest open position, so a deeper referrer rules it out; 'invite-<id>'
submissions are leg joins outright); nothing is auto-added to the queue; and the team link
picks the nearest open position in the member's own leg, in tree order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 06:50:32 -05:00
martbost f18ef592fa Make announce-once survive a deploy, not just a restart
Instrumenting every send found the real shape of this: the live process
sends each payout exactly once, to each feed, with no duplicate to
suppress. So the second copy members were seeing never came from the
running container — it came from ANOTHER one.

During a deploy the outgoing container and the incoming one are both alive
for a moment, and both tail the chain. The announce-once record was held
in memory, so each had its own copy and each announced the same payout,
about a poll interval apart. That matches exactly what the proof channel
showed: identical lines a minute apart, and more of them today because I
deployed four times in half an hour.

The record now lives in its own small file, read fresh and written
atomically on every announcement, so whichever process gets there first is
visible to the other. It also closes the original hole, where the
announcement went out before the state recording it was flushed at the end
of a tick.

Events are a few an hour, so a small read and write per event costs
nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 10:02:14 -05:00
martbost 85a6056354 Announce each payout once, the same way registrations and upgrades already are
Marty saw #49's 4,971.22 POL Gen-4 pass-up from #222 posted twice in the
payments feed. The money is fine: the transaction has one transfer log to
#49's address and one contract payout event, 4971.219256933542 POL, paid
once. Only the announcement doubled.

Payouts were the one event type without the durable announce-once guard.
registered and upgraded are keyed into state.announced — 4,000 entries,
persisted with the rest of the index state. Payouts instead leaned on
state.payouts, which is a rolling window trimmed to KEEP_PAYOUTS (400),
and the announcement was emitted BEFORE that state was written. So a
restart in the gap between announcing and persisting, or any re-read of
the same block, announced the payment again — while an upgrade in the
very same transaction was correctly suppressed. That asymmetry is exactly
what Marty reported: the payout line twice, #222's Apex upgrade once.

The existing state.payouts check stays as the first line of defence; this
adds the durable one behind it, keyed on tx+logIndex, which identifies the
on-chain event exactly.

Not caused by today's deploy — that container started at 14:10 UTC and the
payout landed at 13:40 UTC. Checked before blaming it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 09:31:56 -05:00
martbost 747977b368 Catch eligibility: catcher needs level >= the level the buyer is LEAVING, not the level being bought
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>
2026-09-01 10:51:30 -05:00
martbost 4982897613 Fix missing "who joined / what they unlocked" alongside payout alerts
Marty saw a payout land in the team topic with no registration event beside
it. Confirmed on-chain: #254 registered at 12:49 and the referral payout to
#237 fired at 12:49 — the payout announced, the registration did not. #254 is
inside the org, so the team filter was not the cause.

The bug: registered/upgraded were emitted only when the member was NEW to our
state, or when their stored level was lower than the log said. That conflates
"is this member new to us?" with "have we announced this event?". A snapshot
reads member storage straight from the contract, so if one lands between
someone registering and us scanning that block, the member is already on file
and the announcement is silently skipped — while the payout it triggered goes
out regardless. Exactly the asymmetry reported.

Now keyed on the on-chain event itself (tx + type + id), so each announces
exactly once no matter which code path notices it first. The scan window
still bounds it — we only look past lastBlock — so it cannot replay history.
The set self-trims at 4000 keys.

Also fixed while here: the upgraded event never set `level`, only `newLevel`,
while every consumer reads evt.level. The unlock text only worked because it
could fall back to resolving the level by NAME.

Payout announcements are deliberately untouched: their dedupe depends on
snapshot rows having no tx, so keying them this way risks announcing historic
payouts to everyone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 07:57:11 -05:00
martbost b9ab55a156 Rotation exclusions: config rotationExcludeIds - excluded positions are never offered as the company-rotation target but their subtree is still traversed (founders reserving #4's slot for an incoming team)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 17:09:44 -05:00
martbost 6ebe436cf3 Rotation queue: reconcile against live chain directs in normalizeStatuses - never activate a sponsor the chain says is already 2/2 qualified (also syncs stale direct counts upward)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 14:37:48 -05:00
martbost 9a1833f2b0 Wallet-verified member messaging (matrix-lines permissions, admin visibility)
- 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>
2026-08-19 13:31:31 -05:00
martbost e31b01e338 Dormant company-rotation mode (publicRotationMode=chain, root #2) + multi-root teamRootId alert gating - founders' deal 2026-08-19, flip deferred until team link migration + Marty's right-leg position secured
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 12:39:01 -05:00
martbost c64a716fcd Fix indexer race: instant verify (join-now auto-submission) pre-cached new members, making the log tail skip their registration (referrer directCount never bumped - #41 stuck at 1/2 despite #134). preIndexed flag + merge on event + one-time re-snapshot migration.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 10:50:20 -05:00
martbost 2872a30b96 Coaching scan: expose earnedPol + next upgradeCost on at-risk entries for funded-vs-timing nudge framing
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 06:13:41 -05:00
martbost a835291eab Coaching Radar: live downline triage in admin + member dashboards
- 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>
2026-08-19 05:15:45 -05:00
martbost 9966dc1728 Fix off-by-one in catch-eligibility: catcher must be AT the bought level
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>
2026-08-18 16:54:07 -05:00
martbost b66d3a50e8 Team-depth breakdown: members per generation on admin org panel + member dashboard
- 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>
2026-08-18 16:40:22 -05:00
martbost 1695f712c6 Wire new matrix placements in ~60s, not once a day
The live tail wired a new member into its parent's matrix slots with a
guess-based fill and silently gave up on any fetchMember hiccup, deferring
to the once-daily full snapshot — so a placement could take up to 24h to
appear in the tree (observed: #119 qualified #38 on-chain but the dashboard
card still showed 1/2 with an open slot).

Now placement reads the parent's two slots straight from the contract
(getMatrixChildren) — authoritative and order-independent — and any member
that can't wire on its first pass is queued and retried every tick via
drainPendingWire(). Trees reflect real placements within one ~60s poll.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-17 15:17:39 -05:00
martbost a3e9d05f5d Dashboard 'My Next Step' panel + funded badge
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>
2026-08-17 06:33:59 -05:00
martbost f4481a390b Fix off-by-one level name on upgrade payouts (feed + tweets)
The contract books income atLevel = the level the payer upgraded OUT OF, so
levelName(p.level) showed the FROM level ("upgraded to Scintilla" when the member
actually went to Ascensus). For upline (upgrade) payouts, show the destination
level (p.level+1) — consistent with describeIncome and upgrade.newLevel.
Amounts/generations/recipients were always correct; this is the label only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 18:51:04 -05:00
martbost d0b1eddda2 Show which generation caught each pass-up + finish domain go-forward
Feature (Marty): live payment proof + notifications now show the
generation for upgrade pass-ups (matrix hop distance payer→recipient),
removing the "why did they get it" question. chain.genBetween() adds a
`gen` field to payout events + /api/public/payouts; rendered as a gold
"Gen N" badge in the feed, "Gen N pass-up" in toasts, and "Gen N …" in
Telegram team alerts.

Domain: rmcircle.team is now live (added to Coolify alongside
rmcircle.saasy.top, SSL issued, both serve, no redirect). Switched the
auto-tweet CTA to https://rmcircle.team.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 05:22:47 -05:00
martbost 9717ddbd85 Admin: payment-routing / leak map for the org
New chain.getOrgRouting(rootId, ownerIds) simulates the contract's
_payUpline routing for every member below the org root on their NEXT
upgrade, classifying where the POL lands: your positions, a teammate
in-org (healthy), an outsider above the org (true LEAK), or fees.
Surfaced on /api/admin/income and rendered as a panel in "My Positions
— Income": 4-stat summary + expandable list of payments escaping the
org. Distinguishes real leaks (money leaving the team) from teammates
earning — which the old upgrade-needs alert conflated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 18:35:48 -05:00
martbost d36a405581 Paginate the live payment proof feed (Show more / Show fewer)
The feed showed only the latest 12 of 136 recorded payouts. Now:
- chain.getPayoutsPublic(offset,limit) returns a page of the full
  reversed history plus total/offset/limit/hasMore (limit capped 100).
- /api/public/payouts accepts ?offset & ?limit (defaults 0/40, so the
  live poll + toast detection are unchanged).
- payouts.js keeps a deduped store keyed by payout key; the live poll
  refreshes the recent page while "Show more" pulls older pages 12 at a
  time (with slight overlap so a newly-arrived payout can't open a gap),
  and "Show fewer" collapses back. Verified in-browser: 12 → 48 → 136
  rows, button flips to "Show fewer", collapses to 12, 0 console errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 11:16:12 -05:00
martbost d805a33608 Add home "team by the numbers" showcase + why-a-winning-team-pays
Public /api/public/org-stats (from config.orgRootId, default 21) feeds a
prominent home-page band: share of the whole network, team size,
generations deep, and on-chain POL to the team — populated by bridge.js,
section hides itself if stats aren't ready. Paired with three mechanics
cards (spillover fills your matrix, depth pays as it climbs, the rotation
qualifies you) that make the "winning team" case honestly, plus the risk
caveat ("most participants may not profit") and a disclaimer link.
Live: 51.1% of members, 48 in the org, 10 generations, 25,423 POL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 07:14:23 -05:00
martbost 7b6f629780 Add "Your Organization vs. the Network" live stat to admin
New chain.getOrgShare(rootId) + /api/admin/org-share endpoint compute an
org's share of the whole contract by both member count and total POL
paid, from in-memory state. Admin card (default root 21, saved to
config.orgRootId, auto-loads) shows the two headline percentages, a bar,
and the member/POL breakdown. Verified: #21 org = 51.1% of members,
49.5% of POL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 07:07:27 -05:00
martbost 8b3e82d960 Alert when an owned position needs upgrading to catch incoming pay
New owner upgrade watcher: every 5 min it checks config.ownerIds against
the chain state and, when a leg member is ONE upgrade from paying a
position that isn't eligible yet (below the required level, or not
qualified), emails config.ownerAlertEmail + pings Telegram — "upgrade
#24 to Fabrica, #61 is one upgrade from paying you 2,486 POL." Deduped
per (position, level) in owner-alerts.json, re-fires if the situation
recurs. Same warning renders inline in the admin "My Positions" panel
(upgradeNeeds in the income endpoint). New ownerAlertEmail settings
field. Detection logic unit-tested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 14:27:50 -05:00
martbost 81d90fbe8e Add admin "My Positions — Income" panel
Aggregates every on-chain payment received by a set of owned IDs
(defaults to 21,24,25, saved to config.ownerIds and prefilled/auto-loaded
next time). Shows grand total + per-position totals and a merged,
newest-first ledger (to / from / entry-or-upgrade level / amount) via a
focused chain.getIncome read and an admin-gated /api/admin/income
endpoint. Verified: 5,064.43 POL across 21/24/25, 11 payments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 14:08:21 -05:00
martbost eba405abac Add "Your pipeline" widget to member dashboards
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>
2026-08-14 11:08:07 -05:00
martbost d1aef1e3ff Add /contract — plain-language security review of the smart contract
Shareable page from the 2026-08-13 source review: code immutability
(no proxy/delegatecall/selfdestruct, Sourcify exact match), same-tx
distribution (no pooled funds), locked prices and fee constants, the
honest list of owner powers with the 50% entry-fee cap, where unmatched
pass-ups go, and verify-yourself links. Linked from all public footers,
the payment-proof note, and the chatbot (canned + AI prompt).

Also: passed-over lists now exclude structurally-skipped uplines — a
level-N payment never checks the first N-1 uplines, so listing them as
"not eligible" was misleading.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 10:51:34 -05:00
martbost e3ac27e11c Keep qualification and matrix state live between snapshots
The tail added new members but never updated the referrer's directCount,
the parent's matrix slots, or the recipient's earnedPol — so a member
who qualified mid-day kept showing 1/2 in tree views until the next
daily snapshot (hit live: #63 joined under #35 at 22:56 UTC and #35
still displayed unqualified). Registrations now increment the referrer's
count and wire the new member into the parent's l/r; payouts bump the
recipient's earnedPol. One-time re-snapshot on deploy trues up the
stale state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 18:01:50 -05:00
martbost 130fc42c45 Teach spillover in the matrix views
Tree nodes now carry referrerId, so positions placed by spillover (their
referrer differs from their matrix parent) get a teal "spillover · ref
#N" tag on cards in both the member dashboard and admin matrix, plus a
legend entry. When a member is unqualified but has spillover positions
under them — the classic "why do I have 2 under me but no checkmark?"
state — a callout explains that spillover grows the matrix but only
members joining with their own ID count toward 2/2, and points them at
their share link.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 15:13:22 -05:00
martbost 6a9cd96b8c Qualified dashboards promote the next unqualified position in their leg
Instead of only pointing at the team rotator, a qualified member's share
card now runs the moving-link wave automatically: breadth-first,
left-to-right, the first position below them with fewer than 2 directs
becomes "next in line" — their QR, page link, and join CTA all carry
that position's ID. When it qualifies, the card rotates to the next one.
Falls back to the team rotator when the whole leg is qualified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 14:58:31 -05:00
martbost 350d50798f Member dashboard: drill-down tree, list view, legend, and strategy-aware referral tool
- "Your team" now covers the member's FULL leg with click-to-drill
  pyramid, breadcrumb, and a list-view toggle (mirrors the admin matrix).
- Legend under both the member tree and admin matrix explaining the
  qualified checkmark, tier badges, and downline rollup.
- "Share this position" card: while under 2/2 it's a personal recruiting
  tool — per-member QR (vendored qrcode-generator, CSP-safe), copy link,
  and a join CTA carrying the member's ID as sponsor. At 2/2 the page
  automatically retires the link and redirects sharing to the team
  rotator, so personal referrals can't undermine the moving-link
  strategy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 14:53:06 -05:00
martbost 9f8898d2bd Add public member dashboard at /my/:id
Members enter their on-chain ID (or follow a /my/46-style link) and see
their own position: tier/level/directs/earnings facts, their subtree
pyramid with downline rollups and open slots, classified payment
history, and lineage to root — all chain-derived data only, none of the
admin operational config. Public endpoint /api/public/member is cached
(120s) and rate-limited (20/min/IP). Linked from the public footers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 14:35:05 -05:00
martbost c69ef6c1f6 Show downline count and downline POL on matrix positions
Each tree node now carries a rollup of its subtree — members below and
their combined earned POL — shown as a footer line on pyramid cards and
inline in the list view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 14:28:21 -05:00
martbost 110b9c9dfe Classify income rows as entry vs upgrade instead of raw atLevel
The contract books income atLevel = the level the payer upgraded OUT of
(upgrade N->N+1 records atLevel=N), so an Ascensus upgrade pass-up was
displaying as "Scintilla". Income rows are now classified against the
contract's getAllCosts() tables — upgrade pass-ups arrive at full
upgrade cost, entry referral rewards at slot cost minus the 5% admin fee
— and display "Upgrade to Ascensus" / "Entry — Scintilla" in the admin
income table, public feed, and toasts. Stale snapshot rows re-classify
via a forced re-snapshot on boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 14:18:12 -05:00
martbost 6b17419646 Add sponsor contact emails and Telegram team-build activity alerts
Sponsors can carry an optional contact email (Add Sponsor form, ✉ inline
edit in the queue, shown under the name). New config teamRootId: any NEW
on-chain event at or below that member ID — registration, upgrade, or
payout seen by the live tail — posts to the Telegram group topic, with
the tx link and the sponsor's contact email when one is on file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 12:57:51 -05:00
martbost 1e2b03c795 Render admin matrix as a drill-down pyramid with open slots
Matrix View now defaults to a generation-by-generation pyramid (root + 3
generations of position cards, empty placements shown as dashed open
slots). Clicking a position re-roots the pyramid there; a breadcrumb
navigates back up. The collapsible list stays behind a toggle. Tree
payload now carries explicit left/right children so an only-child lands
on the correct side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 10:36:36 -05:00
martbost 7c41499312 Add on-chain payment proof: live payout feed, ID verification, admin lineage tools
New chain.js reads the RM Circle contract (0x33Bd…2DAF, Polygon) via free
public RPCs — no API keys. Daily snapshot rebuilds complete payout history
from getIncomeHistory (log providers prune old history), and a 60s
eth_getLogs tail catches new payouts with tx hashes, upgrade context, and
passed-over upline IDs.

- Bridge page: "Live payment proof" feed + timed toast pop-ups for payouts
  seen in the last 15 min, every row linking to Polygonscan.
- /start: same toasts; ID submissions are now verified against the contract
  (result shown to the member, in Telegram notify, and in admin).
- Admin: on-chain member lookup (lineage to root, directs, matrix children,
  full income history) and a collapsible full matrix tree view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 10:25:07 -05:00