A Coolify deploy starts the new container and stops the old one only once the new one is healthy, so for about half a minute two copies of this app watch the chain. On 25 September, 06:26 CT, both saw Marty's $5 purchase: two confirmation emails, and the Five Dollar Friday bonus granted twice (+100 and +100, nineteen seconds apart). Every dedupe until now lived in process memory or a JSON file on the volume; a second process shares neither in time. marks.js is the shared version: a row in MySQL that only one INSERT IGNORE can win. The chain-event fan-out claims 'ev:<tx>:<logIndex>' before the email, Telegram post, Friday bonus and sponsor sync; the in-memory feed stays per instance. The Friday scheduler claims its Thursday email, kickoff and wrap-up the same way, with the file marker kept as the second line of defence and the thing an operator can edit by hand. Without a database it degrades to a per-process Set, which is exactly the old single-instance behaviour. Proven on the production MySQL before deploy: first insert 1 row, second 0. Marty's duplicate 100 credits reversed with a labelled adjust row; no other member was double-granted. Co-Authored-By: Claude Fable 5.1 <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.