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:
- a memo of one line of printable ASCII arrives intact;
- some wallets refuse or change binary memos and control characters;
- some wallets refuse a request with a second payment or a zero amount.
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_ifor participanti. It is valid for one round. - Sign-in code
- A one-time code of 8 digits. It moves
k_ito 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
- The participant gives the operator a payout address with an Orchard receiver.
- The operator gives the participant a number
i. - The operator makes a session key
k_ifrom 32 random bytes. - The operator calculates the owner tag τ, a hash of
k_i. - The operator signs
i, τ and a salted hash of the payout address. - The operator sends
i, τ, the salt and the signature to the participant. This step keepsk_iback.
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
- The page makes a 32-byte secret
sand keeps it. - The page sends the hash of
sto the operator. - The operator sends an 8-digit code in a memo to the payout address. The memo shows the first 4 bytes of the hash.
- The participant reads the code in the wallet and types it on the page.
- The page sends
sand the code to the operator. - The operator sends
k_ito the page. The code is then used. - The page calculates the hash of
k_iand 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:
| Code | Status | Check |
|---|---|---|
| 1 | oversize | A field is too long, or the memo has too many commands. |
| 2 | out of bounds | t identifies no participant, or a command is outside the scheme limits. |
| 3 | malformed | The memo fails the grammar, or a new encoding of the memo gives different bytes. |
| 4 | wrong value | The note value is different from the order amount. |
| 5 | expired | x is less than H_e. |
| 7 | bad MAC | The MAC is different from the MAC under k_t. |
| 6 | duplicate | An earlier counted memo has the same pair (t, n). |
| 0 | counted | All 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:
- The participant gives the transaction ID, the action index and the ephemeral secret key
eskof the order note. The wallet getseskwith its outgoing viewing key. - A verifier decrypts the note with
eskand the Inbox receiver. - The verifier calculates
cmxagain and finds the window of the note. - 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
- Each order. The order amount and the network fee. A usual shielded payment with change has two actions. The ZIP 317 fee for two actions is 10,000 zatoshis.
- Wallets. Each order is one transaction on the chain. Put several commands in one memo. See Polite actions.
- Sign-in on a new device. One code request. Its amount pays for the code note and its part of the epoch transaction.
- Wallet support. ZIP 321 with memos. The wallet must send the memo bytes unchanged.
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.
- False orders. The participant can find a false order with the viewing key of the wallet and the memo log of the page. Other persons see only the word of the participant against the word of the operator.
- Copies. A copy of a memo or a QR code is a finished order. The nonce rule counts it one time. The expiry sets how long a copy stays valid.
-
Viewing keys. A holder of the incoming viewing key of the payout address can get a code, and
then
k_i. Use a payout address with a private viewing key, for example a new account. - Phishing. Anyone can send a memo that looks like a code or a payout. A false code fails at sign-in. A clone page can still ask the participant for a real code. Wallets and pages must show the host of each link.
- Privacy. Outsiders see a usual Ironwood-pool transaction. The leaf list shows the number of orders in each window. The operator knows the payout address and the time of each order.
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.