socialpix.club was being used as a free ad board by posting bots; its signup/onboarding/post flows
now redirect here. No legacy credits for these arrivals. Referrer tagging (traffic.js) already covers /from/*.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
/api/my/polhunter signs a member across to polhunter.com with a five-minute single-use HMAC
token (memberId, email, wallet, username); PolHunter has no sign-up and no mailer, this is the
only way in. A PolHunter entry in the dashboard nav. The PolHunter embed on the public ledger
with one slot beside the payout feed; CSP allows polhunter.com.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Marty (2026-09-19): show every missed qualification so the whole chain is visible. The old
notice fired once per passed-up tier and showed only that tier. AdminPaid is the last event of
every purchase transaction (119 of 119 in the index), so the notice now fires there, gathers
every PassedUp and TierPaid of the tx, and missed.js composes one message: each tier's walk,
every person passed over with their buyer count, who finally caught it (or that the company
kept it), and the total POL that walked past how many people on that one sale.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Marty (2026-09-19): cap the login-ad slots. About 124 members sign in a day and every live
slot shares those views, so a fourth slot only thins the other three. Member slots only;
house login ads now step aside whenever a member slot is running (JSON: filtered out of the
pool; MySQL: ORDER BY house ASC). The 14 slots already running keep their term; new buys are
refused until one ends. Format label and chatbot updated.
mindfulmojo (email, 2026-09-19): the viewer frames the advertiser's page and both controls
meant "back"; some sites refuse to load inside a frame and signing up is easier in a full tab.
The viewer bar now carries "Open site in a new tab", set from the ad's target URL.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Marty (2026-09-19): partner codes keep working with no monthly cap, but the credits stop
being a dump. A fifth is paid the day the code is redeemed, then a fifth on each day the
member finishes their daily ad set, five days in all, and whatever is unclaimed 30 days after
redemption is never paid. Same headline number for the partner to promote; paid only to
people who show up. 49 of the 52 PARTNER redemptions were still sitting on 500+ unspent.
Every redemption now records the site the ?promo= link was opened from (iap.promoref cookie
set from the Referer at link-open), so a code's traffic can be attributed to the partner page
it was supposed to come from. Member-funded codes charge the funder one installment at a
time; a short funder leaves the installment due, not lost.
Pre-drip redemptions were paid in full up front and are marked paid=credits at start-up, so
nothing is paid twice. Covered by a module test (13 checks: per-day cadence, gaps, rounding,
expiry, pre-drip rows, declined payer) and an end-to-end HTTP test on a local copy (9 checks).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Marty (2026-09-19): once a membership gets large, My line and the other lists scroll forever.
One helper, pageList(), pages what each pane already renders: rows past the current page are
hidden and a solid-mint control bar (row range, Previous/Next 25, page count) goes under the
list. Row markup and bindings are untouched, so it goes on every growing list at once: My
line per level (the Activity drawer pages with its row, and a chip drill-down opens on the
right page), the Overview roster, the Campaigns table and its credit ledger, each Pipeline
column, and the Messages thread list. Asset tags bumped.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Marty asked for a NAS zone on the two legacy squeeze pages (2026-09-19). Injected server-side
in serveJoinPage for the fw-*/t1-* angles only, below the fine print and above the footer
links; the regular join pages stay ad-free. script-src now allows networkadspace.com, which
it did not, so the tag would otherwise have died silently under the CSP.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fraud-allow.json says "never block this person". There was nothing for the opposite case.
fraud-block.json: an address on it cannot open an account through /api/signup or the
email-code door, and the admin fraud report carries the list with a matching
/api/admin/fraud/block route. Existing accounts are untouched; Suspend covers those.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
today() was a UTC day, which rolls over at 7 PM Central. Sign in at 6:55 PM and again at
7:05 PM and those were two different days, so the once-per-day sign-in bonus paid twice,
ten minutes apart. On the live ledger 83 of the first 364 sign-in bonuses landed less than
24 hours after the previous one, one pair 24 minutes apart, worth 618 credits.
The same boundary governs the daily-credit claim, the claim streak, each campaign's daily
spend cap and the login-ad daily meter, all of which could turn over twice in one Central
evening.
ctDay() puts every member-facing daily rule on America/Chicago, which tracks CST/CDT on its
own. Reporting slices elsewhere are left alone rather than re-bucketing chart history.
The bonus also records login_ts now and refuses to pay again inside 20 hours, whatever the
day string says. 20 rather than 24 so signing in at the same time each morning never skips.
Safe to deploy between midnight and 7 PM Central, when the two day strings agree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The daily meter charged 100 credits for every day a campaign was shown,
with no end date. It produced 74-day queues, and worse, each new campaign
added 100/day of charges while adding no inventory: on 18 September, 15
campaigns were charged 1,500 credits against a platform that had produced
367 login impressions in its entire life. That had stopped being a pricing
inefficiency and become members paying for delivery that did not exist.
Now: 3,000 credits buys 30 days of unlimited impressions, charged once at
purchase, with no delivery promise — the model Marty wanted, matching how
ClickBaitPays sells theirs. The price is deliberately identical to 30 days
at the old daily rate, so nobody pays more than before; they just get a
known end instead of an open-ended drain.
It reuses the `featured` shape, which was already a flat up-front buy that
runs to `expires`. Both daily meters — MySQL and JSON — now skip any
campaign with an expiry, so a slot can never be metered. The existing
sweep closes it on day 30 and returns anything unspent.
qa/login-slot.mjs, 9 checks, including that the slot price still equals
30 days at the old rate and that neither meter can touch a slot campaign.
Member walk clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Marty's call. Sign-ins expired after 24 hours, which meant a member passed
the login gate roughly once a day — and that gate is the entire inventory
behind the login-ad product. The platform has served 356 login impressions
in its life while members have bought 20,526 credits of them.
Four hours, env-tunable via SESSION_TTL_HOURS.
Said out loud in three places rather than left to feel like a glitch:
the login gate itself now explains it, the chatbot has a canned answer for
"why do I keep getting logged out", and the same fact is in the assistant's
system prompt so the AI never calls it a bug. Wording throughout is
Marty's: to keep resources optimised we do not keep anyone signed in
longer than 4 hours, and nothing about the account, credits or line is
affected.
The canned pattern was checked against the neighbours it sits beside —
seven phrasings reach it, and withdraw / pyramid / earnings / second
account / payout answers are all untouched. CANNED is first-match-wins,
so that check matters.
Member walk clean, chatbot suite 35 green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Splitting system messages out of the modal was right for payout receipts,
but it would have silenced two that a member genuinely loses money by
ignoring:
- "@someone is trying to buy. Link your wallet so it pays you" — a sale
is blocked right now, and the referral is lost permanently once it
routes to someone else.
- "You missed 43 POL on InstantAdPay" — a payout passed them by, and the
message explains exactly how to stop the next one doing the same.
So the dividing line is not system-versus-human, it is "does this need you
to do something". Those two become kind 'alert' and still interrupt; the
receipts stay kind 'notice' and stay in the inbox.
The modal no longer credits an alert to a person either. It was saying "A
message from @martbost" over machine-generated text, because system mail
is sent by member 1 at ADMIN_EMAIL. Alerts now read "Action needed on your
account".
Also reclassified "X is now in your line for good" as a notice — it is
good news about a referral the member gained, with nothing at stake.
qa/messages-notice.mjs now covers both lanes: 14 checks, including that an
alert interrupts and a flood of 25 receipts does not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Marty hit this viewing daily ads: a popup after every single return to
the dashboard, each one a different payout notice.
Two causes, both fixed.
The channel was shared. "You just got paid 40.8923 POL" and "Welcome to
my line, here are your first three moves" were stored identically, as
kind 'broadcast' — the kind the sign-in modal is meant to interrupt for.
Dismissing one just promoted the next unread notice, so a backlog became
a carousel. System messages are now kind 'notice': they land in the
inbox, count toward its badge, and never pop. Only a message a person
actually wrote can interrupt.
The modal also had no memory. loadDashboard() runs on far more than
sign-in — after every ad view, campaign edit and chat close — and it
re-popped each time. It now shows at most once per page load and never
twice for the same message.
The 79 existing machine-generated rows are retagged by a migration in
ensureSchema, 15 of them unread and currently popping. Matched on
subject rather than sender on purpose: these come from member 1 at
ADMIN_EMAIL, which is also Marty's own member address, so his genuine
broadcasts sit under the same sender and must be left alone. Verified
against the live data first — "Credits returned: a counting error on our
side" and the broken-banner note are his, and stay as broadcasts.
qa/messages-notice.mjs covers it: a flood of 25 notices produces no
interruption, the human message still does, and chat stays in its own
lane. Member walk clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I read campaigns.received_today as a per-day counter and told Marty the
platform delivers about 2,700 clicks a day. It doesn't. That column is
only a daily figure while the nightly cron resets it, and that reset has
stopped running, so the value is an accumulation since it last ran.
The permanent click ledger is unambiguous: roughly 40 to 50 clicks a day
across the whole platform. It also agrees exactly with ordered-minus-
remaining on every campaign, which is a good independent check that the
read-never-derive design is reading the right thing.
So MAX_CLICKS drops from 10,000 to 2,500. At the real volume a 10,000-click
booking would hold the top of the offerwall for most of a year.
The pack-2 placement is working as intended and is already measurable: the
three backfilled campaigns took 14 of the platform's 17 clicks today.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A campaign's name is the member's own internal label, and some of them are
two characters ("MG"). DripOffers rejects anything under five, and more to
the point a click-earner scanning the offerwall learns nothing from "MG".
So the ad's own headline comes first, then the label, and a very short
label is qualified with the destination host — "MG (mailer.gold)" — rather
than dropped or dressed up in marketing copy we invented on the member's
behalf. Nothing usable at all returns null instead of a made-up title.
Found by the backfill: campaign 88 failed on it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third syndication rail. The first two send impressions; this one sends
clicks — a real person picks the offer off the offerwall, goes to the
member's link, and has to stay the dwell time before anyone is paid.
Two things it does that the other rails can't:
- It carries geo-targeted campaigns. DripOffers filters on an explicit
country list, so a campaign aimed at Tier 1 finally reaches an outside
audience instead of staying on our own site. Tier-3-only campaigns are
skipped rather than quietly sent worldwide, because an inclusion list
can't express "everywhere except the other tiers" and widening it would
deliver exactly the traffic the owner chose to exclude.
- Delivery is counted, not derived. It reads rows from the platform's
permanent click ledger. It never computes delivery as ordered minus
remaining, and pausing never zeroes remaining — that pair is what
charged members for impressions that never ran on Network Ad Space.
Also fixes two live bugs found while wiring it: the AdRevLinks pause was
nested inside the Network Ad Space check in both the end sweep and
setStatus, so with NAS switched off an ended or paused campaign kept
running on AdRevLinks. Each rail is now checked on its own.
Caps: 1,000 credits minimum, 10,000 clicks maximum per campaign. Not for
cost — Marty owns the platform and these placements are free — but because
the whole site delivers around 2,700 clicks a day, and one campaign
booking 50,000 would sit in the list for weeks.
Inert unless DRIPOFFERS_BRIDGE_URL and _KEY are set. 26 checks green
against the live endpoint; member walk clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Second delivery surface for member campaigns, and deliberately the opposite shape to the
Network Ad Space rail: that one is Tier 3 heavy, this one only has prices configured for
US, CA, GB, AU, NZ and UM, so its traffic is Tier 1 by construction.
The AdRevLinks database is not reachable from this server, so rather than opening a
database port to the internet there is a narrow authenticated endpoint on that box which
does the insert locally. Five actions, no general query surface: a leaked key can only
create or remove popup campaigns. The secret lives outside that server's webroot and is
compared in constant time; Apache there strips Authorization, so it travels as
X-Bridge-Key.
adrevlnks.js mirrors nas.js, with two rules carried over from this morning's billing bug:
- DELIVERY IS READ, NEVER DERIVED. status returns the rotator's own per-country counters.
Nothing is computed from a figure a stop could overwrite, which is exactly what charged
16 members for undelivered impressions on the NAS side.
- EVERY WRITE IS IDEMPOTENT. Each campaign carries ref "iap:<id>", so a retry after a
timeout returns the existing campaign instead of booking a second one.
Capped on purpose. That server serves roughly 5,500 popup impressions a DAY in total,
shared by every active campaign, and its rotator favours whichever has delivered least. So
syndicating everything unchecked would starve what is already running, Marty's own ads
included. Hence a credit floor and a per-campaign view cap. Cost is not the reason: he owns
the platform and treats the placements as free. Finite shared inventory is the reason.
Hooked into create, pause/resume and the scheduled end sweep so both networks stay in step.
Inert unless ADREVLNKS_BRIDGE_URL and _KEY are set, and a bridge hiccup can never block a
campaign going live.
qa/adrevlnks-bridge.mjs (15 assertions) drives the REAL endpoint: under-floor campaigns
skipped, full credit value booked, Tier 1 targeting, retry returns the same campaign,
delivery read back, pause/resume mirrored, then deleted and confirmed gone. Creates only
paused campaigns so no live traffic is spent, and leaves nothing behind.
nas-served 8, fraud-allow 12, sponsor-note 5, chatbot-parse 35, qa/run.sh member 0 bugs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A correction that reads 'granted by admin' explains nothing to the member looking at
their ledger. The note now travels with the grant, so a credit-consumption correction
names itself where they will actually see it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Network Ad Space has no stop flag. Its serving query only picks rows with remaining>0,
so zeroing that counter is the only way to halt an ad. But delivery is derived as
assigned - remaining, so the moment an ad was stopped it read back as 100% DELIVERED.
That was not only a reporting error. reconcileNas() runs every five minutes, had no
status filter, and CHARGES member credits off that figure. So pausing a campaign, or
ending one, made the next reconcile pass bill the member for the entire unspent budget
as though it had all been served.
Measured on production before the fix: 23 member campaigns across 16 members, every
single one charged to exactly 100% of budget, 11,437 credits in total, against on-site
delivery evidence of roughly 4,700 impressions. A naturally exhausted ad also ends at
remaining=0, so the two cases cannot be told apart after the fact, which is why the
true figure has to be captured before the stop.
Three changes:
- nas.deactivate() now reads the real served count BEFORE zeroing and returns it.
- a new ads.stopNas() helper is the only path to a stop, and it persists that figure as
the campaign's final delivery. No caller touches nas.deactivate() directly any more.
- reconcileNas() only processes campaigns with status='active'. A stopped ad delivers
nothing further, so there is never anything legitimate left to charge for.
qa/nas-served.mjs (8 assertions) stubs the NAS layer and drives the real code: pausing
records the true 3,000 rather than the 10,000 allocation, a paused campaign is never
charged afterwards and its figure never jumps to the allocation, and an active campaign
still reconciles and is charged normally so the guard did not break delivery.
fraud-allow 12, sponsor-note 5, chatbot-parse 35, qa/run.sh member 0 bugs.
Historical delivery is not recoverable: the stop overwrote the only record of it.
Refunding the 11,437 credits to the 16 affected members is Marty's call, pending.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Marty approves specific people for multiple accounts (partners, staff, a spouse on a
shared machine) and needed a way to say so without the guard fighting him.
Admin > Members > Duplicate signals now carries an "Approved exceptions" list: add an
email with a note, see who is on it and when, remove one. It sits directly under the
signals so the two are read together.
An exception can be added against the address they ALREADY have, not just the new one.
That matters because the usual case is approving a person before their second address
exists, and at sign-up time the new address is unknown to us. checkSignup now tracks
every account the sign-up collided with, and clears the block if either side is approved.
Clearing the HARD flags matters as much as clearing the block. Those flags are what
silently drop an account off the leaderboard and bar it from adopting out of the holding
tank, so an approved person would have been "allowed" in name only. They now keep both.
The account is tagged 'allowlisted' instead, so the admin sees why it went through, and
the server logs the exception by name.
Suspension still wins. An exception is permission to hold several accounts, not immunity
from being suspended for something else.
qa/fraud-allow.mjs (12 assertions) boots its own server and walks the real flow: first
account created, second blocked with the Qualified Start redirect, exception added,
second account now created, no hard flag left on it, exception visible in the admin
report, removing it blocks again, malformed address refused, endpoint admin-only.
qa/sponsor-note.mjs 5, qa/run.sh member 0 bugs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Marty signed in and was told "You're joining the line of @bliss". He is member #1.
Cause: the last-touch sponsor cookie lives 30 days, and /api/sponsor set invited
purely from that cookie. So any member who had ever clicked a teammate's invite link
was greeted on the sign-in screen as though logging in would place them under that
person. Untrue, and alarming in exactly the wrong place: their sponsor locked at their
first purchase and nothing on that screen can move it. Anyone seeing that would
reasonably worry their line was about to change.
The greeting now shows when someone actually arrived through a link (?ref= in the URL),
or when the cookie is present AND this browser has never had an account, which is the
genuine "came back later to finish joining" case. A browser that already has an account,
or a signed-in session, never sees it.
Attribution is deliberately untouched: the cookie still resolves, the sponsor id is
still returned, and placement still works exactly as before. Only the greeting changed.
fraud.hasAccountOnDevice(req) is the new signal, reusing the device cookie the
one-account-per-person checks already set.
qa/sponsor-note.mjs (5 assertions) boots its own throwaway server and creates a REAL
account so the case is proven rather than assumed: still greeted with ?ref=, still
greeted from the cookie on a browser with no account, NOT greeted on the browser that
has one, and attribution still resolving. qa/run.sh member: 0 bugs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-on to the risk rewrite. Two gaps it exposed.
Members ask this without the word PIF: "should I fund their first package", "can I
buy it for them", "can I front someone the $20". Those were falling through to the
AI, which answered reasonably but told the member to "read the full warning on your
training page" - a page section that does not exist and that they would go looking
for. The pattern now also matches a paying verb plus an explicit for-someone-else
phrase, so these get the full canned answer with the risk in it. It deliberately
needs BOTH halves: "which package should I buy" must not be stolen.
The prompt now forbids sending anyone to a warning, guide or page section that is
not in the PAGES list, and requires plain ASCII (the model was emitting non-breaking
hyphens in "Wi-Fi").
Fixed two first-match-wins routing bugs, one of them mine from the previous commit:
"what do I get for buying a package" was returning the commission split, because I
had widened the earnings pattern to "what do I get" when it should only ever have
been "what do I get paid". And "should I buy the $20 package" previously matched
nothing at all and burned an AI call on a question we have a written answer for.
CANNED is an ordered list, so any pattern edit can silently steal a neighbour. The
suite now pins a 19-case routing table across PIF, price, earnings and the tank, so
the next person to widen a regex finds out immediately. qa/chatbot-parse.mjs is 35
assertions. qa/run.sh member: 0 bugs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Marty: the answer has to cover what a member is risking when they send POL to
somebody they have not spoken to and have no commitment from.
The canned answer now leads the advice with "do not send anything until you have
actually talked to that person and they have told you they will use it", then says
plainly: the transfer is wallet to wallet and irreversible, there is no refund and
no chargeback, support cannot pull it back, the gift is theirs whatever they do with
it, and they are free to go quiet or spend it on something else without breaking any
rule. It names the worst case for what it is, which is that a tank member joined
with nobody working with them and may never have engaged, so an unanswered PIF is
the easiest money on this site to lose.
It gives the arithmetic both ways rather than only the upside: a gift that works
costs about half, because the contract pays the sponsor 50 percent when they buy;
a gift to someone who never answers costs all of it. It ends on never gift money
you need and never borrow to do it.
The same guidance goes into the AI prompt as a standing rule, so PIF is never sold
as a tactic on any phrasing that misses the canned pattern, and a "should I PIF
someone from the tank" question starts with talk to them first, not the mechanics.
qa/chatbot-parse.mjs is now 16 assertions: the 9 response-shape cases plus 7 that
pin each risk line, so a future rewrite cannot quietly drop them.
qa/run.sh member: 0 bugs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Marty hit "I hit a snag answering that one" on "Should I pif someone from the
holding tank". Two separate problems behind it.
The real bug: OpenRouter pads a slow upstream with keepalive lines before the JSON,
and when the provider gives up it can close having sent ONLY that padding. HTTP 200,
no body. JSON.parse threw and EVERY non-canned question died the same way, not just
this one. Now we find the actual JSON object in the stream, treat an empty answer as
a failure rather than sending a blank reply, retry once (the usual cause is one
provider dropping the request), and if both attempts fail we say plainly that it was
our side, not their question, and point at a person.
The content gap: PIF and the holding tank were in the system prompt but had no canned
answers, so they depended on the AI being up. Both are now canned and instant. The PIF
answer leads with the thing the question gets wrong, which is that you cannot PIF
somebody still in the tank: adopt first, they link a wallet, then the button appears.
It is honest that nothing obliges them to buy, that crypto transfers are irreversible,
and that the 50 percent coming back only happens if they actually purchase.
Also fixes a pre-existing routing bug found while testing: "how much do I earn"
returned the PACKAGE PRICE ladder, because the price pattern matches "how much do"
and the commission pattern only caught "how much earn".
qa/chatbot-parse.mjs (9 assertions) drives the real parse branch against a stub
server for every response shape: keepalive-only, keepalive-then-JSON, plain JSON,
SSE comments, empty content, non-JSON. qa/run.sh member: 0 bugs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The member card showed the wallet address on its own, directly above "Registered:
no (payouts off)". That reads as "he is set up" when he is not, and it caused a
real misread today on @mcbit1: wallet linked, memberId 0 on chain, every sale in
his line walking up to his sponsor while the row looked healthy.
Linking a wallet is a free signature that tells the site which address is theirs.
Switching on payouts is a separate transaction that creates the position. Only the
second one makes them payable, so the two states now say so:
Main wallet 0x30a7…5703 payouts OFF
Registered no wallet linked, but payouts were never switched on, so no
position exists. Sales in their line walk up to their sponsor and
lock there.
and the no-wallet case says "no wallet linked yet" instead of the same text.
qa/run.sh member: 0 bugs (the 2 warnings are pre-existing and unrelated).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Marty, 2026-09-16, after @megamol created megamol2/megamol3 under his own link and bought $20 on each
to fake his two qualifying buyers. fraud.js records sign-up IP/UA/browser id (iap.dev cookie set with
the code request) and last-seen on sign-in. New accounts: dup-device (browser already has an account)
and sponsor-device are refused, ip-burst (> fraudMaxSignupsPerIpDay, default 2, per 24h) is refused;
sponsor-ip and shared-ip are flagged only. Flagged/suspended accounts never count on the leaderboard
and cannot adopt from the tank; suspended accounts are signed out everywhere (auth.fromRequest
wrapper) and refused at sign-in. Admin > Members: Duplicate signals card (shared browser / IP,
flagged, suspended), flags badge, Suspend/Unsuspend; GET /api/admin/fraud; PATCH members {suspend,
reason, flags}. Telegram admin alert on every block/flag. Privacy page + chatbot prompt updated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Audit against the live site: removed stale text (featured links and visit packs marked "coming",
Pipeline "coming soon", the superseded sponsor-hold rule, a non-existent home-page calculator,
"earned credits spend on banner and text only", "Coaching pane"); added live featured and visit-pack
rules with prices and limits, the video cap and rewards, the sign-in bonus and claim ladders,
report-an-ad, the public missed-payout posts, the walk-up gain notice, the four-minute overview
video, the promo Videos and Toolkit tabs; new canned answers for featured links, visit packs and
"what is this". KNOWLEDGE_DATE constant + qa/chatbot-sync.mjs (run in public/all QA) fails when a
release note is newer than the chatbot's knowledge.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Campaign #134 (NAS ad 2806) had the member's join-page link in the image field and served a broken
banner 141 times on the network; #102 did the same with an imgbb page link. imageCheck() accepts
/uploads/ and instantadpay.com/banners files and image extensions outright, otherwise HEADs the URL
(one redirect) and requires image/*. Wired into member and house campaign creation for banners and
login-ad creatives. Form placeholder + hint point at Promo tools > Banners > Copy image URL.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Marty, 2026-09-16: make missing a payout visible. On a TierPaid with hops>0 the PassedUp events of the
same tx name the skipped positions, with the reason and how many qualifying buyers they had; the post
says the amount and who received it instead. On a Purchase whose buyer was routed past an unactivated
sponsor (movedByTx set by sponsorSyncOnEvent), the post names the sponsor who missed the whole sale
and the upline who now owns that buyer for good. Both go to the main feed and the shared payments
topic unless the events mode is 'none'.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>