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>
InstantAdPay — site
Membership advertising with immutable on-chain settlement. Zero-dependency Node server (RM Circle pattern): static pages + JSON API + SSE ledger.
Architecture
server.js— http server: pages,/api/*,/join/<id>sponsor links, SSE feedchain.js— contract reader + persistent event indexer (free public RPCs)auth.js— SIWE wallet sign-in (EIP-4361), sessions in the volumeaccounts.js— site-side records only (free members, last-touch sponsor attribution, handles). The CHAIN is the source of truth for money/credits.public/— landing, live ledger, member area; no client libraries
Chain flip (rehearsal → mainnet)
Everything chain-specific lives in data/config.json (volume):
{contract, chainId, chainName, explorer, rpcs, deployBlock}.
Defaults point at the Amoy rehearsal deployment. Launch = deploy the
mainnet contract, wipe accounts.json/sessions.json/chain-index.json,
PATCH /api/admin/chain with the mainnet values, set rehearsal:false via
/api/admin/site. Same code, different config.
Run
PORT=3100 node server.js # DATA_DIR defaults to ./data
Admin API auth: Authorization: Bearer $ADMIN_PASSWORD.
Spec: ../CONTRACT-SPEC.md (v1.0.3-frozen). Contracts: ../contracts/.
Chatbot knowledge is part of every change
chatbot.js answers members two ways: CANNED regex answers and systemPrompt() for the AI. Any
change that a member could ask about (a new pane, a rule, a price, a limit, a fix they noticed) is not
done until both are updated in the same commit, and KNOWLEDGE_DATE in chatbot.js is bumped. The QA
runner (bash qa/run.sh public) fails when a release note on the live site is newer than that date.