Documentation
cashr, end to end.
Everything an organiser, attendee, door volunteer or self-hoster needs: how to use cashr, what it protects, what it costs, and how it works underneath.
To try every step without ZEC, use the demo at demo.cashr.site: the same app on a simulated chain. To try it on a real chain with free test coins, use testnet.cashr.site with a testnet wallet and ZEC from the faucet.
cashr is RSVPs for groups whose attendee list would hurt people if it leaked: support groups, union organisers, journalists and their sources, activists. A small refundable deposit in shielded ZEC stops no-shows. The deposit hides who paid, the ticket is a secret that lives only on the attendee’s phone, and the refund goes back to an address only the attendee knows.
The result: the server never has a list of people to leak. For an event it holds the event’s details, random ticket hashes, one-time refund addresses and payment records (the full list is on the threat model page), and it deletes them when the organiser purges the event.
Hosting an event
- 01Create the eventOn Create event, set a name, time, capacity and refundable deposit (minimum 0.001 ZEC). The place is public: for sensitive venues, write something like “sent on Signal after you RSVP”.
- 02Save your two private linksYou get a manage link and a door link, shown once. cashr stores only a hash of them, so it can't recover them. Keep them in a password manager. You can also choose to remember them on this device.
- 03Share the public RSVP linkSend it however your group already talks. Anyone with it can RSVP until the event is full or you close RSVPs.
- 04Check people inOpen the door link on the phone at the entrance and scan tickets. It works offline and syncs when the connection returns.
- 05End the event & refundOn the dashboard, End event & refund sends everyone who checked in their deposit back. Deposits of people who didn't come are forfeited, and swept to your group's fund address if you set one.
- 06PurgeOnce refunds are done, Purge deletes every ticket hash, refund address and payment record. Only totals remain.
If everyone comes, there are no forfeited deposits to pay network fees. The dashboard will ask you to send a small top-up (about 0.001 ZEC, no memo) to the event address, and refunds then go out automatically.
RSVPing
You need a Zcash wallet that holds shielded ZEC and can pay a payment link with a memo, such as ZODL.
- 01Open the inviteOpen the link, or paste it on the Join page.
- 02Give a refund addressIn your wallet, open Receive and copy the shielded address (it starts with
u1). Use a fresh one: an address used for another RSVP would link the two. The page checks it can receive a private (Orchard) refund before you pay. - 03Pay the depositScan the QR code or tap Open in wallet app. The amount and memo are filled in; don't change them. Your ticket shows as confirmed within about three hours (often sooner): the public list updates at random moments, so nobody can time yours.
- 04Keep your ticketYour ticket appears as a QR code. Save the image: it is stored only in this browser, and it is the only way in and the only way to get your deposit back.
- 05Show it at the door, get refundedAfter the event, the deposit returns to your refund address.
Your tickets are listed on the Tickets page, on this device only.
| If your payment is returned | Why |
|---|---|
| The event was full | Seats go in block order. Returned minus 0.0001 ZEC. |
| The amount didn't match | Every RSVP must pay exactly the deposit, so payments look identical on chain. |
| RSVPs had closed | The organiser closed RSVPs or the deadline passed. |
| The refund address was already used for this event | Each ticket needs its own refund address. |
| The memo was changed | Without the memo there is no ticket. A payment with no readable refund address can't be returned. |
At the door
Open the door link on a phone and tap Start scanning. Each ticket shows Welcome in, Already checked in, Not a valid ticket or Wrong event. You can also paste a ticket code by hand.
The door keeps a copy of the event’s ticket hashes, so it keeps working if the connection drops. It queues check-ins and syncs them when it’s back online. It never sees names or refund addresses, and it records whether a ticket was used, not when.
How it works
- 01One wallet account per eventEach event gets its own account from its own random seed (encrypted at rest), with an Orchard-only (shielded) address and a birthday at the current block. Purging the event deletes the seed.
- 02A secret on the attendee's phoneThe browser makes 32 random bytes and keeps them. Only SHA-256 of that secret leaves the device.
- 03The memoThe payment carries one encrypted line:
CSH1 <event> <ticket hash> <refund address>. Only the event’s wallet can read it. - 04The wallet reads itA light-client wallet syncs from lightwalletd, fetches the full transaction over Tor (after a random delay) to decrypt the memo, and the API records the hash and the refund address's Orchard receiver.
- 05The door checks a hashThe ticket QR holds the secret; the door hashes it and looks the hash up.
- 06Refunds stay shieldedEach refund is its own fully shielded send to the attendee's Orchard receiver (through the Ironwood pool since the NU6.3 upgrade) with the FullPrivacy policy, at a random time within about three hours, so no amount, recipient or group of attendees is revealed.
Why only Zcash can do this: cards and PayPal give the organiser a name; Bitcoin and Ethereum put payer and refund on a public ledger. Shielded Zcash hides sender, receiver and amount, and carries the refund instructions inside the payment.
Deposits & fees
| Deposit | Set by the organiser, minimum 0.001 ZEC. Refunded in full to everyone who checks in. |
| Returned payments | Refunded minus 0.0001 ZEC, so junk payments can't drain the event's fees. |
| Network fees for refunds | Paid from forfeited deposits and organiser top-ups, never from refunds. |
| When a refund counts as paid | Once its transaction is mined with 3 confirmations. One that expires unmined is sent again automatically; it is never paid twice. |
| No-show money | Swept, minus the network fee, to the organiser's no-show fund address once refunds have confirmed. Without an address it waits in the event wallet, and the event can't be purged until it is swept. |
| When refunds can go out | A deposit becomes spendable after 10 confirmations (about 12 minutes; about 4 once the NU7 upgrade makes blocks three times as frequent). Refunds queued earlier simply wait. |
| Who holds the money | cashr's wallet holds deposits until the event is settled. Keep deposits small: the price of a coffee. |
Privacy
The server never stores names, emails, phone numbers, check-in times or ticket secrets, and the API keeps no access logs. (Your hosting provider’s edge may still log IP addresses; use Tor for high-risk events.) Organiser and door links keep their secret after the #, which browsers never send to any server. Attendees never send the server anything that points at their ticket.
Read the full threat model for what is stored, every known leak and how it is closed, and what you are trusting.
Self-hosting
cashr is three pieces: this web app, a FastAPI service, and cashr-wallet, a light-client Zcash wallet built on librustzcash that syncs from a public lightwalletd server over Tor, so no full node is needed.
# On a fresh Ubuntu server, as root: wallet + API + HTTPS, secrets generated for you curl -fsSL https://raw.githubusercontent.com/linoxbt/cashr/main/deploy/install.sh | bash -s -- api.example.com # Back up the wallet volume and, separately, CASHR_WALLET_SEED_KEY. Both are needed to move funds.
Put HTTPS in front of the API, then deploy web/ to Netlify or Vercel with NEXT_PUBLIC_API_URL set to the API’s URL. Run exactly one API process: it refuses to start if another holds its database. Full settings are in docs/DEPLOY.md.
API reference
JSON over HTTPS. Organiser and door routes take Authorization: Bearer <token>.
| Method & path | Auth | Does |
|---|---|---|
GET /meta | — | Chain mode, network, current block, reject fee. |
POST /events | — | Create an event. Returns the event, the admin token and the door token (once). |
GET /events/:id | — | Public event details and counts. |
GET /events/:id/tickets | — | Every ticket hash, plus returned payments. Statuses appear only once the event settles. |
GET /events/:id/admin | Admin | Everything stored for the event, the wallet balance and payouts. |
POST /events/:id/admin/:action | Admin | Actions: close, reopen, settle, retry, purge, scan. Purge refuses while money is left in the event wallet. |
POST /events/:id/admin/fund | Admin | Body {"address"}. Set the no-show fund address; a settled event’s leftovers are swept to it. |
GET /events/:id/door/manifest | Door | Confirmed ticket hashes for offline checking. |
POST /events/:id/door/checkin | Door | Body {"ticket_hash"}. Result: ok, already, unknown or event_closed. |
POST /events/:id/door/sync | Door | Upload check-ins recorded offline. |
FAQ
No. They see how many tickets were checked in, never whose. The deposit's sender is hidden even from the event's own wallet.
It can't be recovered: no one else has the secret. Save the ticket image when you RSVP.
The event keeps working for attendees, but no one can settle it. Funds stay in the event's wallet account, which only the server operator can move.
Paying from a transparent balance exposes your address. Pay from your shielded balance; refunds always go to a shielded address.
The public list updates at random moments, up to about three hours apart, so nobody can tell when your payment arrived. If it still isn't listed after that, check the amount and memo were unchanged.
No. It holds only hashes of tickets, and the door needs the secret behind the hash.