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

  1. 01
    Create the event
    On 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”.
  2. 02
    Save your two private links
    You 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.
  3. 03
    Share the public RSVP link
    Send it however your group already talks. Anyone with it can RSVP until the event is full or you close RSVPs.
  4. 04
    Check people in
    Open the door link on the phone at the entrance and scan tickets. It works offline and syncs when the connection returns.
  5. 05
    End the event & refund
    On 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.
  6. 06
    Purge
    Once 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.

  1. 01
    Open the invite
    Open the link, or paste it on the Join page.
  2. 02
    Give a refund address
    In 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.
  3. 03
    Pay the deposit
    Scan 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.
  4. 04
    Keep your ticket
    Your 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.
  5. 05
    Show it at the door, get refunded
    After 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 returnedWhy
The event was fullSeats go in block order. Returned minus 0.0001 ZEC.
The amount didn't matchEvery RSVP must pay exactly the deposit, so payments look identical on chain.
RSVPs had closedThe organiser closed RSVPs or the deadline passed.
The refund address was already used for this eventEach ticket needs its own refund address.
The memo was changedWithout 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

  1. 01
    One wallet account per event
    Each 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.
  2. 02
    A secret on the attendee's phone
    The browser makes 32 random bytes and keeps them. Only SHA-256 of that secret leaves the device.
  3. 03
    The memo
    The payment carries one encrypted line: CSH1 <event> <ticket hash> <refund address>. Only the event’s wallet can read it.
  4. 04
    The wallet reads it
    A 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.
  5. 05
    The door checks a hash
    The ticket QR holds the secret; the door hashes it and looks the hash up.
  6. 06
    Refunds stay shielded
    Each 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

DepositSet by the organiser, minimum 0.001 ZEC. Refunded in full to everyone who checks in.
Returned paymentsRefunded minus 0.0001 ZEC, so junk payments can't drain the event's fees.
Network fees for refundsPaid from forfeited deposits and organiser top-ups, never from refunds.
When a refund counts as paidOnce its transaction is mined with 3 confirmations. One that expires unmined is sent again automatically; it is never paid twice.
No-show moneySwept, 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 outA 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 moneycashr'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 & pathAuthDoes
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/adminAdminEverything stored for the event, the wallet balance and payouts.
POST /events/:id/admin/:actionAdminActions: close, reopen, settle, retry, purge, scan. Purge refuses while money is left in the event wallet.
POST /events/:id/admin/fundAdminBody {"address"}. Set the no-show fund address; a settled event’s leftovers are swept to it.
GET /events/:id/door/manifestDoorConfirmed ticket hashes for offline checking.
POST /events/:id/door/checkinDoorBody {"ticket_hash"}. Result: ok, already, unknown or event_closed.
POST /events/:id/door/syncDoorUpload check-ins recorded offline.

FAQ

Can the organiser see who came?

No. They see how many tickets were checked in, never whose. The deposit's sender is hidden even from the event's own wallet.

What if I lose my ticket?

It can't be recovered: no one else has the secret. Save the ticket image when you RSVP.

What if the organiser loses their links?

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.

Can I use a transparent (t-address) balance?

Paying from a transparent balance exposes your address. Pay from your shielded balance; refunds always go to a shielded address.

Why does my ticket say Not listed yet?

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.

Can a seized server be used to get in?

No. It holds only hashes of tickets, and the door needs the secret behind the hash.

Testnet · real Zcash test network, test coins with no value · Get test ZEC · Real app at cashr.site
cashr
--:--
Chain: …