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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- "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>
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>
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>
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>
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>
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>
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>