Skip to content

Chat: require holding 10 XCP to post - #59

Merged
droplister merged 2 commits into
mainfrom
claude/new-session-ta1vaw
Oct 13, 2026
Merged

droplister merged 2 commits into
mainfrom
claude/new-session-ta1vaw

Conversation

@droplister

Copy link
Copy Markdown
Member

Signing in is free, so bans and the per-author cooldown only bound identities that cost nothing to replace. Someone in chat was demonstrating exactly that ("rotating identity" to evade bans and rate limits). Posting now requires the wallet to hold at least 10 XCP. Nothing is spent.

What counts

Confirmed XCP balance plus XCP the wallet has paid into launches that are still minting (escrowed, not gone). XCP in open DEX orders does not count. Addresses in CHAT_ADMIN_ADDRESSES are exempt.

Enforcement (server-side only)

  • apps/web/src/lib/chat-eligibility.ts (new): holdsChatMinimum(address). Verdicts are cached per isolate: 10 minutes for a holder (so the same 10 XCP moved between wallets unlocks them slowly) and 1 minute for a wallet under the bar (so a top-up is picked up soon after it confirms).
  • POST /api/chat checks before publishing, using the address from the signed session cookie. Under the minimum returns 403 insufficient_xcp. An unreadable balance falls through as 503 unavailable: never a pass, never a false refusal.
  • GET /api/chat adds holdsMinimum (true/false/null) for display only.
  • fetchAssetBalanceOrThrow (Counterparty) and fetchMintingXcpPaid (our index, /v2/mints/by/:source, paged to exhaustion) both throw on failure rather than reading as zero.

UX

  • Not connected: unchanged, "Connect a wallet to chat."
  • Connected, under 10 XCP: "Must hold 10 XCP to chat." with Send disabled. Reading and muting still work.
  • A post refused later (balance dropped) shows the same message and keeps the draft. Reopening chat re-checks, so a top-up reopens the composer.
  • New string translated in all locales (marked machine-drafted in *.status.json), with a translator note that it means holding, not paying.

Testing

  • New tests/chat-eligibility.test.ts: 10 XCP inclusive, escrow counted, cache TTLs, failures throw and aren't cached.
  • tests/chat-route.test.ts: refusal before the room, unreadable balance → 503, admin exempt, GET holdsMinimum.
  • tests/chat-panel.test.tsx: message shown and Send blocked; refusal on send keeps the draft; reopen after top-up re-enables posting.
  • Verified against the live node proxy that a zero-XCP wallet returns [] (200), so it reads as 0 rather than as an error.
  • Chat suites 247/247; tsc, eslint, numeric and i18n checks pass.

Deploy

Web only (npm run deploy:web). No API, schema or config changes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LkjKD3QZivr3Qr2cKdjfbz


Generated by Claude Code

Signing in is free, so a ban or the per-author cooldown only bound an
identity that cost nothing to replace. Posting now requires the wallet
to hold at least 10 XCP: its confirmed balance plus XCP it has paid into
launches that are still minting, which is escrowed rather than spent.

The web route checks before publishing and reports an unreadable balance
as unavailable, never as a pass or a refusal. Verdicts are cached per
isolate: ten minutes for a holder, one minute for a wallet under the bar.
Configured chat admins are exempt. The chat identity read tells the
composer whether the wallet qualifies, so a wallet under the bar sees
"Must hold 10 XCP to chat." instead of a message box; reopening chat
re-checks after a top-up. Logged-out visitors still see "Connect a
wallet to chat."

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkjKD3QZivr3Qr2cKdjfbz
A post refused for the minimum now marks the shared identity instead of a
composer-local flag, so reopening chat re-checks and a top-up clears it
in one place. holdsMinimum is a required field, documented as display
only: the server checks every post.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LkjKD3QZivr3Qr2cKdjfbz
@droplister
droplister merged commit 43588cb into main Oct 13, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants