Docs
Use
- Try it. The box on the home page gives 3 free messages a day per network, on the fast model. No wallet.
- Buy coupons. In Booth → Buy, pick a pack and connect an injected wallet (MetaMask, Rabby, or a wallet app’s built-in browser). The page switches you to Robinhood Chain (4663), checks your balance, sends one ETH payment to the treasury and asks you to sign one claim message.
- Chat. Each fast message spends 1 coupon; each deep message spends 3. The wallet is not used again.
- Back up. Coupons live only in this browser. Use Export coupons for a JSON file, Import to load it elsewhere. The file spends like cash.
- Forget. Delete one conversation or everything, any time. Our server has no copy to delete.
Models: fast = Claude Haiku 5.5, deep = Claude Sonnet 5.5. Messages go through OpenRouter to Anthropic’s Claude models, each request restricted to zero-data-retention endpoints (zdr: true, docs). Conversations are sent in full with each message (the server is stateless), so long booths are trimmed from the oldest turn to fit the limits below.
Blind coupons, step by step
Suite RSABSSA-SHA384-PSS-Deterministic from RFC 9474, 2048-bit modulus — the same idea as Privacy Pass. Library: @cloudflare/blindrsa-ts@0.4.6, on the server and (bundled, unminified) in your browser.
- Your browser draws N random 32-byte messages with
crypto.getRandomValues. With that much entropy the deterministic variant is allowed by RFC 9474 — no extra randomising step needed. - It blinds each one with the published public key. Before paying, it saves
{msgs, inv, blinded}to IndexedDB, so a reload can finish the claim. - Your wallet pays the treasury, then signs the claim text below. It binds the transaction to this exact batch, so nobody watching the chain can claim your payment.
POST /api/issue→ the server checks the tx on chain, recovers the signer (must equaltx.from), and blind-signs the batch. Re-claiming with the same batch returns identical signatures (deterministic RSA); a different batch for the same tx is refused.- Your browser unblinds and verifies every coupon against the key it cached. A failed check is shown, not saved.
- Coupon =
{epoch, msg, sig}(base64). To chat, the browser sends 1 or 3 coupons. The server verifies, reservessha256(msg)in its spent table in one transaction, then calls the model. If the model fails before the first token, the reservation is deleted (refund). After the first token the coupon is final.
The server never sees the unblinded msg until it is spent, and nothing in the spent coupon relates it to the blinded value it signed.
Messages your wallet signs
Plain personal_sign, exact text, lowercase hex. Smart-contract wallets can’t produce a signature that recovers to the paying address, so use a regular account.
Buying (ETH payment, or a burn once the token exists):
MUMBOOTH issue v1
chain: 4663
tx: <txHash lowercase>
batch: <sha256 hex of concat(blinded bytes, in order)>
count: <N>
Daily holder claim (once the token exists):
MUMBOOTH hold v1
chain: 4663
address: <0x… lowercase>
day: <YYYY-MM-DD UTC>
batch: <sha256 hex>
count: <K>
The signing key everyone shares
Blind signatures only hide you if everyone gets coupons from the same key. Compare this fingerprint with the one posted on X at launch. Your browser stores it and warns if it ever changes.
Loading key…
Fingerprint = sha256 of the SPKI DER bytes. Your browser recomputes it from the key data rather than trusting the label. Check yourself: base64 -d <<< "$SPKI" | shasum -a 256.
Endpoints
All JSON. Errors look like {"error": "<code>", "message": "<human>", …detail}. No CORS: the API is for this site.
| Endpoint | Body | Answer | Errors |
|---|---|---|---|
GET /api/status | – | price, treasury, counters, ETH received (from chain), token info | chain down → still 200 with ethReceivedWei: null |
GET /api/keys | – | {current, keys:[{id, alg, modulusBits, spki, fingerprint, issueUntil, redeemUntil}]} | – |
POST /api/issue | {address, txHash, blinded[], sig} | {keyId, n, blindSigs[]} | bad_request · too_many_coupons · underpaid · wrong_recipient · unsupported_tx · tx_failed · tx_not_found (pending, retry) · bad_signature · signer_mismatch · tx_used · not_launched · rpc_unavailable · rate_limited |
POST /api/hold/claim | {address, day, blinded[], sig} | same as issue | not_launched · below_minimum · already_claimed · bad_day · too_many_coupons · bad_signature · rpc_unavailable |
POST /api/chat | {mode:"trial"|"coupon", model:"fast"|"deep", coupons[], messages[]} | SSE: meta → delta… → done; mid-stream error {code, spent:true} | context_too_long · coupons_required · coupon_invalid · coupon_spent · key_expired · trial_exhausted · trial_paused · rate_limited · model_unavailable (no coupon taken) |
GET /api/proof | – | last 100 purchases (tx links), holder claims, issued, spent | – |
Limits: up to 40 messages, 8k characters each; total 24k (fast) / 48k (deep) / 6k (trial). Answers up to 1,024 / 2,048 / 512 tokens. Roles alternate and end with the user. After 5 model failures in a row the server pauses chat for 60 s and answers model_unavailable without taking coupons.
Once the token exists
Today, coupons are bought with ETH. The rules below apply from the moment the token’s contract address is published (it replaces 0x000…000 on the home page). Until then the server answers not_launched.
- Burn for packs. Transfer tokens to
0x000000000000000000000000000000000000dEaD. The server reads theTransferlog from the token contract and issuesfloor(amount ÷ tokensPerCoupon)coupons for that transaction, with the same signed claim as an ETH purchase. - Hold for daily coupons. A wallet holding at least the minimum balance can claim a fixed number of coupons once per UTC day by signing the hold message. No transaction, no gas.
- Splitting a balance across many wallets to claim more is limited by the minimum: each wallet needs its own full minimum.
- Holder claims are public by nature (an address and a day). The coupons they produce are blind-signed like any other.
The browser code is not minified
Everything that runs in your browser is served as written. Read it before you trust it. The server, the nginx config and the tests are public too: github.com/mumbooth/mumbooth (MIT).
- /js/blind.js — blinding, unblinding, verification
- /js/wallet.js — chain switch, payment, the exact claim text
- /js/coupons.js — IndexedDB: coupons, pending claims, history, key cache
- /js/api.js — every request this page makes
- /js/chat.js — chat, coupon spending, safe text rendering
- /js/booth.js — the booth app
- /js/vendor/blindrsa.js —
@cloudflare/blindrsa-ts@0.4.6, bundled without minification
No third-party scripts, fonts or analytics are loaded; the Content-Security-Policy only allows this domain, plus the public Robinhood Chain RPC used when adding the network to your wallet.