Making games with Zcash is easy as pie with Zcash Payment Hooks.
I wanted to learn how Zcash Payments Hooks work and I figured that there'd be no funner way than by vibecoding one of those addictive match-3 games. The results was Six Seeds. My kid is obsessed with Percy Jackson so I with a Greek Mythology genre for the game. You play the goddess Persephone, escaping the underworld one chamber at a time. The first chamber is free. To cross into each of the other five, you pay Charon (the mythical ferryman who carries souls across the river Styx) a toll of 0.001 ZEC from any shielded wallet, and the next chamber opens once the payment confirms.
I wanted to see if it is possible and practical for a small game to accept private payments with no payment processor and no app store in the middle? The answer is yes! All it takes is a small server, a lightweight Zakura node, and the receiving daemon free2z published.
The game
Six Seeds is a Svelte app wrapped in Tauri, which turns a web app into a native desktop program. The macOS build is a 3 MB download. The same code runs in a browser, and on an iPhone it installs to the home screen.
It is not in the App Store, on purpose. Apple's guideline 3.1.1 requires in-app purchase for unlocking game levels and explicitly rules out unlocking them with cryptocurrency. The web and the desktop are where a Zcash toll can live.
One server, four parts
Six Seeds (browser, iPhone, Mac) │ 1. ask for an invoice 4. poll until unlocked ▼ ▲Unlock service ◄── 3. payment events ── zcash-paymentd │ ▲ viewing key, private journal │ 2. ZIP-321 link: address, │ full blocks over RPC │ amount, memo "unlock:<token>" │ ▼ │Player's wallet ──► Zcash mainnet ──► Zakura 1.7.0, pruned
| Part | What it does |
|---|---|
| Zakura 1.7.0, pruned | Follows and validates the chain. Bootstrapped from the official 11 GiB pruned snapshot. |
| zcash-paymentd | Recognizes payments to the game's address with a viewing key. It holds no spending key. |
| Unlock service and hook | Issues invoices, records payment events, and decides which chamber a payment opens. Standard-library Python and SQLite. |
| Caddy | Serves the game and its API over HTTPS. |
All of it runs on one virtual machine with 2 vCPUs, 8 GB of RAM and a 100 GB disk. The game never talks to the node directly. It only asks the unlock service for an invoice, then asks again whether its chamber is open.
How a crossing works
- The game asks for an invoice and shows a ZIP-321 link and QR code: my address, the toll, and a one-time memo like
unlock:p8sckj3b6cwdsx. - The player pays from any shielded wallet.
- When the payment lands in a mined block,
zcash-paymentdemitspayment.observed, and the game says the oar has dipped. - After three confirmations it emits
payment.confirmed. The hook records the unlock against that player, and the game says you have crossed.
The hook follows the delivery contract from Part 2 of the free2z series. It deduplicates by event id, ignores stale revisions, re-locks a chamber if a reorganization revokes the payment, and records underpaid or memo-less payments without ever unlocking anything.
The first crossing
On October 12, Chamber 2 opened for a real mainnet payment of 0.001 ZEC (100,000 zatoshis) that I sent from my phone:
| Step | Time (UTC) |
|---|---|
| Invoice issued | 22:51:38 |
| Payment seen in a mined block | 22:56:07 |
| Three confirmations, chamber unlocked | 22:57:22 |
The hook received each event exactly once, with no failures or retries.
What I learned
- On iPhone, iOS picks the wallet, not you. Any installed app can claim
zcash:links, and iOS chooses among them with no menu. Mine opened an exchange app instead of the Zcash wallet I wanted. The toll now has separate copy buttons for the address, amount and memo, so any wallet works. - The memo is the product. No memo, no unlock. The game says so next to the copy buttons.
- Charge only for levels people can beat. I simulated 500 attempts per chamber on the real game engine. The last two chambers were winnable only about 20% of the time, which is a bad thing to charge for. I retuned them to about 60% and 50%, and a test now fails if a paid chamber drifts out of range.
- A payment is not DRM. Level data ships inside the game, so the server knows who paid but cannot stop a modified client from playing anyway. Content that lives on the server, such as video, can be truly withheld until the payment confirms.
- Nodes have expiry dates. Every Zakura release stops at a fixed block height. Zakura 1.6.0 would have stopped at block 3,536,467, so I upgraded to 1.7.0, which runs until early December. Running your own node means upgrading on the chain's schedule, not yours.
Caveats
This is a demo on real mainnet ZEC. zcash-paymentd is early operator software without a security audit. The node is pruned and was bootstrapped from a snapshot, which means trusting that snapshot's state, as Part 1 of the free2z series explains.
If a small game can take private payments with a node, a daemon and two short scripts, your project can too. Try it at 34-24-229-200.sslip.io, and bring 0.001 ZEC for the ferryman.

