Applied Zcash

Pattern

Wallet-native orders

A participant sends app orders from a usual Zcash wallet. Each order is a ZIP 321 payment with a memo. The wallet shows the memo before the participant approves the payment.

A MAC under a session key identifies the participant. The keys that control the funds of the participant stay in the wallet.

Problem

An app must know which participant sent each order. A shielded payment hides its sender. Wallets sign messages with transparent keys at most. ZIP 304 covers only Sapling keys.

Thus, many apps ask for a seed phrase, or keep a secret key in the browser. Participants with real funds refuse to put a seed phrase in a web page.

Wallets already send ZIP 321 payments with memos. A wallet shows the memo before it sends the payment. This pattern uses these two functions. A survey of wallets in November 2026 found these limits:

Mechanism

Roles

Operator
The service that runs the app. It receives orders and publishes the result.
Participant
A person who sends orders from their own Zcash wallet.
Page
The web client of the operator. It shows payment requests and status.
Inbox
A unified address with an Orchard receiver. The operator publishes it and receives all orders there.
Session key
A 32-byte key k_i for participant i. It is valid for one round.
Sign-in code
A one-time code of 8 digits. It moves k_i to one page.

Sequence

participant                page                          operator
     │                        │                               │
  1  ├── join: payout address ───────────────────────────────→ │ i, k_i
  2  │                        ├── hash of secret s ──────────→ │
  3  │ ←──────────────── memo: 8-digit code ─────────────────── ┤
  4  ├── types the code ────→ ├── s and code ────────────────→ │
  5  │                        │ ←──────────────────────── k_i ─┤
  6  │ ←── QR code: ZIP 321 ──┤  memo + MAC under k_i          │
  7  ├── payment to the Inbox ──────────────────────────────→ │ status of each note
  8  │ ←──────────────────── payout to the payout address ───── ┤

Join

  1. The participant gives the operator a payout address with an Orchard receiver.
  2. The operator gives the participant a number i.
  3. The operator makes a session key k_i from 32 random bytes.
  4. The operator calculates the owner tag τ, a hash of k_i.
  5. The operator signs i, τ and a salted hash of the payout address.
  6. The operator sends i, τ, the salt and the signature to the participant. This step keeps k_i back.

Each Orchard receiver can join one time in a round. The operator then sends a welcome to the payout address. The welcome can carry funds for the first orders.

Sign-in

  1. The page makes a 32-byte secret s and keeps it.
  2. The page sends the hash of s to the operator.
  3. The operator sends an 8-digit code in a memo to the payout address. The memo shows the first 4 bytes of the hash.
  4. The participant reads the code in the wallet and types it on the page.
  5. The page sends s and the code to the operator.
  6. The operator sends k_i to the page. The code is then used.
  7. The page calculates the hash of k_i and compares it with τ. The page also examines the signature.

Only a person who can read the payout address gets a code. A code is useful only with the secret of the page that asked for it. The operator stops a code after the first correct try or after the fifth incorrect try.

A new device needs a new code. The participant pays a code request to the Inbox from the wallet. The memo of the request carries the new hash and the payout address.

Order memo

The page writes each order as one line of printable ASCII:

memo = scheme " r=" round " t=" i " n=" nonce " x=" expiry
       " | " command [" | " command]… " | mac=" mac

round   first 4 bytes of the round identifier, hex
i       participant number
nonce   4 random bytes, hex, new for each memo
expiry  the last close height at which the memo counts
mac     MAC_k_i(P_mac, all bytes before " | mac="), 8 bytes, hex

The MAC is keyed BLAKE2b with an 8-byte output. The scheme of the app sets the commands and the personalization P_mac. The memo is a ZIP 302 text memo of 512 bytes or fewer.

Payment request

For each memo, the page makes a ZIP 321 payment request with one payment: the Inbox, the order amount and the memo. The page shows the request as a QR code. The page also shows the URI, the address and the memo for copy.

A request is a finished order. The first payment of a request places the order. Thus, the page shows a request only when the participant asks for it.

Windows and status

The rules set a lag c in blocks. Epoch e closes at height H_e. Epoch e counts the Inbox notes at heights h with:

H_(e-1) - c  <  h  ≤  H_e - c

Each Inbox note is in one window. Anyone who knows the close heights can calculate each window. A reorg of fewer than c blocks keeps each closed window the same.

The operator gives each Inbox note one status. It examines the notes in chain order. The first check that fails sets the status:

CodeStatusCheck
1oversizeA field is too long, or the memo has too many commands.
2out of boundst identifies no participant, or a command is outside the scheme limits.
3malformedThe memo fails the grammar, or a new encoding of the memo gives different bytes.
4wrong valueThe note value is different from the order amount.
5expiredx is less than H_e.
7bad MACThe MAC is different from the MAC under k_t.
6duplicateAn earlier counted memo has the same pair (t, n).
0countedAll the checks above pass.

Only counted memos use a nonce. Only a holder of k_t can make a valid MAC. Thus, each participant controls their own nonces.

Record and disclosure

For each window, the operator publishes a leaf list. Each leaf is the cmx of one Inbox note and its status code. The operator puts the Merkle root of the list in the record of the epoch, for example in a bound note. The leaf list shows which notes went to the Inbox. It keeps the senders and the memos secret.

A participant can show that the operator omitted an order or gave it an incorrect status:

  1. The participant gives the transaction ID, the action index and the ephemeral secret key esk of the order note. The wallet gets esk with its outgoing viewing key.
  2. A verifier decrypts the note with esk and the Inbox receiver.
  3. The verifier calculates cmx again and finds the window of the note.
  4. The verifier examines the leaf list. A missing leaf shows an omission. A status that disagrees with the memo and the record shows an error.

The disclosure opens one note only. The other notes of the wallet stay secret.

Payout

After the round, the operator pays the final balance of each participant to the payout address from the join.

Cost

Limits

WARNING: The operator knows k_i. Thus, the operator can make a false order. Use this pattern only for apps with small stakes, for example credits. Apps with real stakes need signatures from the wallet of the participant.

Spec

The full spec is a draft ZIP. It includes the grammar of code requests, the code quota, the lifetime of codes and the open questions. This site will publish the draft after review.

Demo: a toy poll

A toy poll uses this scheme:

scheme   toypoll1
command  "q" question SP answer
         question 1 to 9, answer YES or NO
limit    9 commands in each memo
amount   1,000 zatoshis (0.00001 ZEC)
P_mac    "ToyPoll_ordmac_1"

The demo uses Rust compiled to WebAssembly. It writes the memo, calculates the MAC, makes the ZIP 321 URI and draws the QR code. The session key and the nonce are random values from your browser.

CAUTION: The Inbox is a Testnet address. Its key is in the public source of this site. A wallet on Testnet can pay the request, but the operator is only a demo.

Order

The WASM module is loading.