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>
This commit is contained in:
martbost
2026-09-17 05:27:18 -05:00
parent 8ad2e01a74
commit 6c8cc0ede2
2 changed files with 97 additions and 8 deletions
+32 -8
View File
@@ -20,7 +20,7 @@ function key() {
function enabled() { return !!key(); }
// ---- canned answers: instant, free, always in voice ----
const KNOWLEDGE_DATE = '2026-09-16'; // bump whenever the canned answers / prompt are brought up to date
const KNOWLEDGE_DATE = '2026-09-17'; // bump whenever the canned answers / prompt are brought up to date
const CANNED = [
{ re: /(second account|another account|two accounts|multiple accounts|more than one account|duplicate account|new account with|refer myself|self.?refer|sign up (my )?(wife|husband|partner|spouse|kids?|family))/i,
a: 'One account per person, and that is in the Terms (https://instantadpay.com/terms). A second account under another email, name or wallet is not allowed, and neither is referring yourself through one. The one sanctioned way to hold more than one position is Qualified Start: extra wallets linked inside your single account, on the Buy packages tab, where they are visible as yours. A family member who genuinely wants to join can join under your link with their own email. Duplicate accounts can be merged or closed and any credits or prizes earned through them are forfeited; payments the contract already made cannot be reversed.' },
@@ -28,7 +28,9 @@ const CANNED = [
a: 'Fair question. A pyramid pays for recruiting with no product; here every payment is for real ad delivery, bought by real advertisers, and the whole money flow is public. Check the code and every payout yourself at https://instantadpay.com/contract and https://instantadpay.com/ledger.' },
{ re: /(withdraw|cash ?out|payout request|minimum payout)/i,
a: 'There are no withdrawals here, ever. The contract pays your share straight to your own wallet in the same transaction as the purchase. Nothing to request, nothing to wait on.' },
{ re: /(how (much|do i) earn|commission|percent|split)/i,
// "how much do I earn" used to fall through to the PRICE answer below, because that one
// matches "how much do" and this one only caught "how much earn" (Marty, 2026-09-17).
{ re: /(how (much|do i) earn|how much (do|can|will|would|could|might) (i|we|you|they) (earn|make|get)|what (do|would|will) i (earn|make|get)|commission|percent|split)/i,
a: 'Every package splits the same way: 50 percent to the direct sponsor, 20 percent to level 2, 10 percent to level 3, 20 percent to the platform. Those numbers are constants in the contract and cannot be changed. The package ladder is on the home page. No income is promised; nobody earns unless real ad buying happens.' },
{ re: /(balance (went|going|keeps going) down|credits? (disappear|missing|gone|dropping|went down|ticking)|why (is|did) my (balance|credits)|in campaigns|set aside|reserved credits|available to spend)/i,
a: 'One rule: your balance is what is NOT committed to a live campaign. When you start a campaign its whole budget is set aside right then (earned credits first, then purchased), so your Available number drops once and then sits still while the ad runs. The budget spends down inside that campaign in Members > Campaigns, where you can see delivered views, network views and what is left. The dashboard shows Purchased available, Earned available and In live campaigns as three separate numbers; a paused campaign keeps its unspent budget set aside so it can resume. Nothing is lost when a balance reads low while a campaign is live. Purchased credits live on the blockchain; if we ever refund or credit purchased money it shows under Purchased as "credited to you" and spends like a purchase, login ads included. Earned credits fund every format except login ads. Two formats are flat buys paid in full when booked: featured links (sold by the day) and verified-visit packs. Adding credits to a featured link does nothing, so its row offers Extend run instead, which books more days at the daily rate; a visit pack offers Buy more visits. Every credit that comes in or goes out is listed with its reason under Members > Campaigns > Credit activity (also linked from Earn credits): sign-in bonus, daily claim, video watched, verified visit, campaign buys, views delivered per day, grants and refunds. Check it before assuming credits went missing.' },
@@ -36,6 +38,13 @@ const CANNED = [
a: 'Every purchase and every share is on the chain, and you can read it yourself: open Members > Earnings > Trace a payment, enter the member number or username (yourself, anyone up to 3 levels below you, or your own uplines), and each purchase they were part of is laid out level by level: who was paid, who was skipped and why. The three usual answers: the person had not unlocked that level when the purchase settled (level 2 needs 2 qualifying $20+ buyers, level 3 needs 5, and qualification never applies backwards); the buyer used Qualified Start, so the purchase came from a linked wallet whose sponsor is the buyer\'s own main position, which means level 1 went to the buyer themselves; or the person\'s payouts were not switched on. Skipped shares pass up to the next qualified person, never to nobody.' },
{ re: /(sponsor (never|hasn'?t|has not|didn'?t|did not|won'?t|hasnt|didnt) (switch|turn|activat|link)|(payouts?|wallet) (not |aren'?t |isn'?t |never )?(switched|turned|set) on|unactivated sponsor|not activated sponsor|routed past|walk(ed|s)? up|goes? to the company|company position)/i,
a: 'Your purchase never waits on your sponsor. If the person who referred you has not linked a wallet and switched on payouts when you buy, the site walks up your sponsor line to the first person who is activated with payouts on and the purchase goes through under them; if nobody in the line is ready it goes under the company position. You see a one-line note on the Buy tab and carry on. For the sponsor it is permanent: the contract locks a buyer to whoever was paid at the first purchase, so that referral moves to the upline who was paid and they cannot earn from that wallet later. They get a message saying so, and the upline gets one too. Two minutes on the Wallet tab prevents it.' },
// PIF + the holding tank. Asked together often ("should I pif someone from the tank"),
// and the honest answer is that the order matters: you cannot PIF someone still in the
// tank. Canned so it never depends on the AI provider being up (Marty, 2026-09-17).
{ re: /\b(pif|p\.i\.f|pay(ing)? it forward|paid it forward|gift (them|him|her|someone|a member)|send (them|him|her) (some )?pol)\b/i,
a: 'PIF is a gift you send from your own wallet, and the site never touches the money. It only appears on your own free referrals and on members you have adopted, and only once they have linked a wallet, so you cannot PIF somebody who is still sitting in the holding tank. The order is: adopt them from Members > My line > Holding tank, get them to link a wallet on their Wallet tab, then the PIF button shows up, you enter an amount and your own wallet app opens with their address already filled in. Whether you should is your call: nothing obliges them to buy anything, and crypto transfers cannot be reversed, so only gift what you are happy to lose. If they do buy a $20 package with it, the contract pays you 50 percent of that purchase as their sponsor and they become one of your qualifying buyers, but there is no guarantee they buy at all, so talk to them first rather than sending POL to somebody who has gone quiet.' },
{ re: /(holding tank|adopt(ing|ion|ed)? (a |an |from|someone|member)|orphan(ed)? member|no sponsor|unsponsored|release to tank)/i,
a: 'The holding tank is where free members who joined without a sponsor wait, and anyone can be picked up first come. To adopt you need payouts switched on and your own $20 or larger package bought; then open Members > My line > Holding tank. You can hold 2 open adoptions at a time and each one runs for 7 days, and if that person never links a wallet or buys they drop back into the tank. Adopting sets you as their sponsor, opens a chat and emails them, and their first purchase binds to you on the chain. You can also release one of your own free referrals into the tank for somebody else to work with, though not one you adopted less than 3 days ago. A member can be adopted twice in their lifetime at most, so an adoption you drop still costs them one of those chances.' },
{ re: /(qualif|unlock level|level 2|level 3|pass.?up)/i,
a: 'Two different things are earned here. CREDITS: every member, free ones included, earns ad credits by viewing ads, videos, visits and the daily sign-in; credits buy your own ads and are never withdrawn. POL: real crypto paid to your wallet when your referrals buy packages, once you are activated: the $20 starter package plus payouts switched on from your wallet. Level 1 pays 50 percent of the packages your direct referrals buy, from the day you are activated. Bring 2 buyers of $20 or more and level 2 unlocks; 5 unlock level 3. When a level is not qualified, its share climbs the sponsor line, checking up to 25 positions, and pays the first qualified person. Qualification never expires and cannot be bought. Qualified Start: you may link extra wallets of your own as positions under your account (Buy packages > Qualified Start); each one that buys a $20+ package counts as a qualifying buyer, its credits pool with yours, and 50% of its purchase comes back to your main wallet. Your own money, your own wallets, a faster start, never an income promise.' },
{ re: /(free member|without (a )?wallet|do i (have to|need to) buy|earn (in )?credits|credits (vs|versus|or) pol|earn pol|get paid in)/i,
@@ -169,8 +178,17 @@ function askAI(message, historyText) {
let d = '';
res.on('data', c => d += c);
res.on('end', () => {
try { resolve(JSON.parse(d).choices[0].message.content.trim()); }
catch (e) { reject(new Error('openrouter ' + res.statusCode + ': ' + d.slice(0, 150))); }
// 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 then threw and every question died on "I hit a snag" (Marty, 2026-09-17).
// Find the actual JSON object, and treat an empty answer as a failure worth retrying.
const i = d.indexOf('{');
if (i < 0) return reject(new Error('openrouter ' + res.statusCode + ': empty body (' + d.length + ' bytes of keepalive)'));
let txt = '';
try { txt = String(JSON.parse(d.slice(i)).choices[0].message.content || '').trim(); }
catch (e) { return reject(new Error('openrouter ' + res.statusCode + ': ' + d.slice(i, i + 150))); }
if (!txt) return reject(new Error('openrouter ' + res.statusCode + ': empty content'));
resolve(txt);
});
});
req.on('error', reject);
@@ -184,11 +202,17 @@ async function answer(message, historyText) {
if (!msg) return { error: 'Say something first.' };
for (const c of CANNED) if (c.re.test(msg)) return { reply: c.a, canned: true };
if (!enabled()) return { reply: 'Good question. The quick answers I have cover packages, payouts, qualification, and joining; for anything deeper, the plain-language contract review at https://instantadpay.com/contract covers most of it.' };
try { return { reply: await askAI(msg, historyText) }; }
catch (e) {
console.error('chat AI failed', e.message);
return { reply: 'I hit a snag answering that one. The contract review at https://instantadpay.com/contract and the live ledger at https://instantadpay.com/ledger cover most deep questions.' };
// One retry: the usual failure is a single provider dropping the request, and asking
// again almost always lands. Only after both fail do we admit defeat.
for (let attempt = 0; attempt < 2; attempt++) {
try { return { reply: await askAI(msg, historyText) }; }
catch (e) {
console.error('chat AI failed (attempt ' + (attempt + 1) + ')', e.message);
if (attempt === 0) await new Promise(r => setTimeout(r, 700));
}
}
// Never a dead end: say plainly that it was us, and offer a person.
return { reply: 'That one did not come back to me just now, and that is our side, not your question. Ask me again in a moment and it should go through. If it is urgent, the plain-language contract review at https://instantadpay.com/contract and the live ledger at https://instantadpay.com/ledger cover most of the deep questions, and your sponsor can be reached from Members > My line.' };
}
module.exports = { init, answer, enabled, KNOWLEDGE_DATE };