I have been thinking about where ZIP-321 fits as Zcash starts supporting richer wallet-to-application experiences.
ZIP-321 solved an important problem. A URI such as zcash:<address>?amount=1&memo=... gives wallets a standard way to understand payment requests. A merchant can put it in a link or QR code, a wallet can parse it, and the user gets a transaction ready to review.
That is useful, and I do not think ZecAuth should be framed as a replacement for ZIP-321.
The limitation is that ZIP-321 is fundamentally a payment-description format. It answers “what should I pay?” but it does not authenticate the interaction around that payment. There is no authenticated requesting domain in the format, no nonce, no expiry, and no cryptographic proof that the user approved a particular request in a particular application context.
That starts to matter once we move beyond simple donation QRs and into merchant checkout, connected applications, recurring interactions, ramps, or anything where a wallet maintains a relationship with a service.
The original ZIP-321 design discussion actually touched on this. Trust in the QR was largely contextual: where did the user see it, does the order number look right, does the payment make sense? That is reasonable for a simple URI standard, but it means that if somebody replaces a merchant QR with another perfectly valid ZIP-321 QR containing an attacker-controlled address, the wallet can confirm that the replacement URI is valid. It cannot determine from ZIP-321 itself that this was not the merchant’s intended request.
QR codes make this particularly interesting because users tend to assign trust to the physical QR itself. Research presented at USENIX Security 2025 found a significant difference in how often users noticed malicious payment information when it was hidden behind a QR compared with manually presented payment details. The wider crypto ecosystem has also seen malicious payment QRs and deep links used in phishing campaigns.
That does not make QR codes bad. It means a QR is an encoding mechanism, not an authentication mechanism.
Where ZecAuth is different
ZecAuth approaches the problem as a wallet authorization protocol rather than another payment URI format.
The design separates authentication, permission to request payments, approval of individual transactions, disclosure of wallet information, address disclosure, and actual spending authority. We intentionally do not treat all of those as one generic “connect wallet” permission.
Authentication starts with a short-lived challenge containing the domain, URI, network, random nonce, issue time and expiration time. The default lifetime is five minutes. The wallet signs a canonical human-readable challenge using a dedicated RedPallas authentication key, and the server verifies both the signature and the expected domain and chain.
So instead of simply receiving an identifier from a wallet, the application receives cryptographic proof that the wallet approved a specific authentication challenge bound to a particular domain context, network, nonce and time window.
Replay protection is also part of the model. The nonce is signed and short-lived, while the relying server tracks whether it has already been consumed. A request cannot simply be captured and replayed indefinitely.
That does not mean a photographed QR becomes magically harmless. If somebody captures a still-valid request before it is consumed, there can still be a race. The important difference is that the request has a bounded lifetime and can be made single-use instead of remaining a reusable static instruction.
Payment approval is also different from payment description
This is probably the part of ZecAuth that matters most when comparing it with a static payment URI.
A ZecAuth transaction request contains the requesting domain, recipient, amount, chain, request ID, issue time and expiration time, along with optional memo, description and additional transaction context.
The wallet presents that request to the user. When the user approves it, the wallet signs the canonical request together with the decision:
Decision: approved
If the wallet has already broadcast the transaction, it can also bind the transaction ID into the same signed approval.
A rejection is signed as:
Decision: denied
The verifier reconstructs the expected signed message and requires an exact match. You cannot take a signed denial and relabel it as an approval. You cannot replace a signed transaction ID. You cannot change the recipient or amount without invalidating the signature because those values are part of the request that was approved.
That gives the relying application something a static payment QR does not provide: proof that this ZecAuth identity approved this exact transaction request.
There is also a useful permission boundary here. A dApp can receive permission to request a payment without receiving permission to spend. The wallet still prompts the user for individual payments.
“Allowed to ask me to pay” and “allowed to spend my money” are deliberately different capabilities.
Privacy is not only an on-chain problem
This is the part I think is especially relevant to Zcash.
When we talk about privacy, we naturally focus on the blockchain. Can someone see the sender, recipient, amount, or transaction graph?
But privacy can be lost before a transaction ever reaches the chain.
A payment URI can contain an address, amount, memo and other descriptive information. If that URI is pasted into a chat, stored in logs, captured in a screenshot, placed on a clipboard, photographed, or backed up by a messaging service, that metadata now exists outside the blockchain.
A shielded transaction cannot make an old screenshot private.
The application layer therefore needs its own privacy model.
One of the privacy mechanisms we are building into ZecAuth is domain-scoped authentication identities. Instead of giving every application the same permanent ZecAuth public key, the wallet derives a separate authentication identity for each domain.
The same wallet seed therefore produces a different ZecAuth identity for shop.example and exchange.example.
Those applications cannot correlate the user simply by comparing ZecAuth public keys.
This matters because otherwise a wallet authentication protocol could accidentally create exactly the kind of global identifier that a privacy-oriented ecosystem should avoid.
Wallet access should be selective, not all-or-nothing
The other privacy decision is to make disclosures explicit.
A ZecAuth application can request specific capabilities such as authentication, requesting payments, viewing a balance snapshot, viewing transaction history, seeing incoming payments, accessing a receiving address, or obtaining a full viewing key.
These have very different privacy consequences.
A one-time balance snapshot is not the same thing as transaction-history access. Incoming-payment visibility is narrower than full history. A Unified Full Viewing Key gives ongoing read-only visibility and is significantly more sensitive than either of those. Sharing a receiving address also creates its own correlation risks.
The scope model treats these as separate permissions and identifies wallet-data disclosures such as balance, history, incoming payments, viewing keys and addresses as privacy-sensitive.
The user can therefore see exactly what an application is asking for rather than being presented with one ambiguous “connect wallet” action.
I think that is a much healthier model.
The better question is not simply “is this website connected to my wallet?”
It is “what exactly did I allow this website to know or ask me to do?”
Requested and granted permissions are also intended to become part of the authorization that the wallet cryptographically commits to. That means an application should not be able to turn a narrow authorization into a broader one after the user approves it.
The authorization becomes closer to: “For this domain, on this network, for this period, I grant these specific capabilities.”
Address disclosure needed its own primitive
We are also introducing a signed AddressBinding.
This lets a wallet prove that a particular receiver was intentionally disclosed by a particular domain-scoped ZecAuth identity. The signed statement includes the domain, ZecAuth identity, address, address type, network, purpose, nonce and expiration time.
Importantly, it carries no spending authority.
That creates a useful distinction between proving “I control this ZecAuth identity” and proving “this identity intentionally disclosed this specific receiver to this service for this purpose.”
This becomes useful for exchanges, ramps and merchant systems that need a receiving address but should not automatically gain access to balances, transaction history or a viewing key.
There is still an unavoidable privacy trade-off here. If the same Zcash address is disclosed to multiple applications, those applications may be able to correlate the user through that address even if the ZecAuth identities themselves are different.
And if that address is transparent, its activity is publicly observable on-chain.
In the ramp work we are doing, that distinction is explicit. A dApp can request a transparent address specifically for a zec_ramp purpose, and the transaction context can explicitly identify that the requested flow uses a transparent receiver.
That is not a privacy guarantee. It is a privacy disclosure.
I think protocols should make those boundaries visible rather than pretend they do not exist.
Authentication itself stays off-chain
ZecAuth authentication does not require a Zcash transaction.
There is no authentication transaction fee and no authentication event that needs to be written to the blockchain. Simply signing into a service therefore does not create another public on-chain event.
But “off-chain” should not be confused with “anonymous.”
The application still knows that someone connected to it. Its server may see IP and network metadata. If the user grants balance, history, address or viewing-key access, the application sees exactly what the user granted.
ZecAuth is not a substitute for Tor, good server-side privacy practices, shielded transactions or careful metadata handling.
The goal is narrower: authentication should not automatically require revealing unrelated wallet information.
Domain binding also needs an authenticated origin
There is another distinction that matters when designing this properly.
A wallet signing a challenge that contains example.com proves that the wallet approved an authorization bound to example.com. That alone should not be treated as proof that whoever generated the challenge owns example.com.
The surrounding transport also needs to establish the origin.
For example, if a request is fetched over HTTPS, the wallet can verify that the domain in the request matches the authenticated origin from which that request was retrieved.
So the two layers work together: the transport proves where the request came from, while the ZecAuth signature proves what the wallet authorized for that origin.
That prevents “domain” from becoming nothing more than a self-declared string inside a signed message.
What about deep links?
The safest ZecAuth flow is to open a wallet you already trust and scan the request from inside that wallet.
In that case, the operating system is not deciding which wallet should handle an arbitrary payment link. The user already selected the wallet before the request was interpreted.
After connection, the application can send requests through the established session instead of presenting a new payment QR every time.
If zecauth:// is opened externally as a deep link, however, it is still a custom URI scheme and the normal OS-level URI-handler considerations remain. So the goal is not to claim that custom URI schemes somehow become safe because ZecAuth exists.
The more interesting change is that the protocol does not need every application interaction to depend on them.
A user can establish an authenticated wallet session once, and subsequent requests can travel through that session.
I think ZIP-321 and ZecAuth solve different layers
For a simple donation QR, invoice or static payment request, ZIP-321 can be exactly the right tool. It is compact, interoperable and easy for wallets to understand.
ZecAuth becomes useful when the interaction needs state.
Who is the wallet interacting with? How long is the request valid? Has it already been consumed? What did the user approve? What information did the wallet disclose? Is the application allowed to request payments? Can a disclosed address be cryptographically tied back to the authenticated identity? Can two unrelated applications interact with the same wallet without automatically receiving one global identifier?
Those are not really URI-format questions.
They are authorization and privacy questions.
So I do not see ZecAuth as replacing ZIP-321. I see them as operating at different layers.
**ZIP-321 standardizes what a wallet should pay. ZecAuth is an attempt to standardize the authenticated, privacy-aware interaction around that payment.
**
We would like feedback from the Zcash community
We are still actively building ZecAuth, and one of the reasons for sharing this now is to get feedback before the design becomes too rigid.
The goal is not to assume that we have identified every wallet-authentication, payment, or privacy problem that needs solving. We would especially like input from wallet developers, protocol engineers, security researchers, merchants, and anyone who has worked with ZIP-321 or wallet integrations in practice.
Are there attack surfaces or privacy leaks we have missed? Are there parts of the authorization model that should be simpler or stricter? Are there wallet UX problems around payment requests, permissions, address disclosure, deep links, replay protection, or transaction approval that ZecAuth should address before we consider the protocol mature?
We are also interested in edge cases that only show up in real implementations. For example, multi-account wallets, hardware wallets, mobile deep-link behavior, offline signing, multiple devices, session revocation, compromised dApps, receiver rotation, shielded-only applications, or anything else that may affect how this should work in practice.
If there are existing Zcash implementation problems that you think an authentication and authorization layer should solve, please bring them up as well. Even if the answer is that ZecAuth should deliberately not solve a particular problem, defining that boundary early is useful.
The intention here is to build something that fits naturally into the Zcash ecosystem rather than design a protocol in isolation and ask wallets to adapt to it later.
So I would really appreciate criticism, edge cases, alternative approaches, and examples of problems you have encountered with existing wallet-to-application flows.
What are we missing? What would you want solved before you would feel comfortable implementing something like ZecAuth in a Zcash wallet or application?