Commit Graph

13 Commits

Author SHA1 Message Date
martbost cb6da01e1c Stop billing members for syndicated impressions that were never delivered
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>
2026-09-17 19:52:12 -05:00
martbost e95f9df13a Approved exceptions: people you have okayed to hold more than one account
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>
2026-09-17 10:42:31 -05:00
martbost 2dfccf0039 Sign-in no longer tells an existing member they are joining somebody's line
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>
2026-09-17 08:41:46 -05:00
martbost f27388a322 Chatbot: catch the PIF question when nobody says "PIF", and stop inventing references
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>
2026-09-17 05:41:54 -05:00
martbost 3867b28bd4 Chatbot: the PIF risk is the answer, not a footnote
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>
2026-09-17 05:36:42 -05:00
martbost 6c8cc0ede2 Chatbot: the PIF and holding-tank answers, and no more dead ends on a provider hiccup
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>
2026-09-17 05:27:18 -05:00
martbost 362526d007 Chatbot brought fully current + knowledge guard
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>
2026-09-16 14:10:00 -05:00
martbost 02f742f90a Pipeline: sponsor follow-up board (stages from the ledger, notes/follow-ups/tags, stage messages into chat), gated by pipelineMode with a coming-soon card until launch; training cards can wait on the same switch
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 11:01:03 -05:00
martbost b3523e9dc4 Release notes + roadmap: /whats-new page, Overview What's new card, Admin > Releases editor
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 04:54:43 -05:00
martbost 4efba20d4e Earn: video stops when leaving the tab/pane, 'all watched today' message, open earn tokens survive a redeploy; admin member search matches as you type, clearer old-site line; QA earn harness follows the done screen
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-13 06:46:08 -05:00
martbost 49a98e7ee3 Blog: admin-written coaching articles, server-rendered public /blog with SEO metadata, sitemap, RSS, robots
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 18:42:56 -05:00
martbost 261f90d843 QA: walk the Traffic and P&L admin panes too
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 07:59:40 -05:00
martbost 8b25361eef QA harness: qa/run.sh + walk.mjs (public + member/admin) + earn.mjs (earning flows)
Headless Playwright checks that boot a throwaway local copy for anything that
signs in or writes; npm run qa / qa:public / qa:member / qa:earn.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 11:32:13 -05:00