zkPayDocs

THE ESSENTIALS

A guide to zkPay.

zkPay uses zero-knowledge proofs to let you spend your private balance without revealing which deposit you are spending.

YOUR FIRST PAYMENT

Two steps, one private balance.

  1. Deposit SOL

    Connect a supported wallet, unlock your private balance with a message signature, then deposit SOL. Confirm the deposit transaction in your wallet.

  2. Send privately

    Enter a recipient’s Solana address and an amount. Review what the recipient will receive, then send. Your browser creates a proof and the relayer submits the payment.

The recipient needs only a Solana address. They do not need to connect to zkPay.

Try it on Mainnet

DEVELOPER QUICK GUIDE

SDK integration.

Add private SOL deposits, balance queries, and sends to your own product with the zkPay TypeScript SDK. You build the interface and connect the wallet; the SDK handles local proof generation and interactions with the zkPay pool.

Currently supports native SOL on Solana Mainnet. We plan to expand support in future releases.

1. Build from source

git clone https://github.com/zkPayonSOL/zkpay-sdk-solana.git
cd zkpay-sdk-solana
npm ci
npm run check

This checks the types, runs offline tests, and builds dist/. The example below imports that build from a file in the repository root. Adjust the import path when using another project.

2. Supply a wallet and RPC

  • Wallet: supply a WalletSigner with publicKey, deterministic signMessage, and v0 signTransaction support. Wallet connection and approval screens belong to your application.
  • RPC is required: pass exactly one of rpcUrl or an existing Solana Connection as connection. There is no default RPC or automatic fallback.
  • Free public RPC is allowed: you can explicitly choose https://api.mainnet.solana.com. Public endpoints are shared and rate-limited; see Solana’s RPC guidance before choosing one for production.

Your RPC handles chain reads, deposit broadcast, and transaction-status checks. The separate apiUrl selects the indexer and relayer; it defaults to zkPay’s Mainnet API. Supplying an RPC does not replace those services.

3. Unlock and read a balance

Node.js / server-side TypeScript example. Your application supplies the signer and RPC URL. Calling this helper requests an unlock signature and reads Mainnet state; it never submits a transaction.

import { ZkPayClient, formatSol } from './dist/index.js';
import type { WalletSigner } from './dist/index.js';

export async function readBalance(wallet: WalletSigner, rpcUrl: string) {
  const client = await ZkPayClient.create({
    network: 'mainnet-beta',
    wallet,
    rpcUrl,
  });
  try {
    await client.unlock();
    const balance = await client.getPrivateBalance();
    return {
      totalSol: formatSol(balance.balanceLamports),
      spendableSol: formatSol(balance.spendableLamports),
    };
  } finally {
    client.dispose();
  }
}

The circuit can spend at most two notes per payment, so the amount spendable in one payment can be lower than the total balance. Keep private balances and unlock signatures out of public logs.

4. Add Deposit and Send

Use an unlocked, live client for the payment flow. Pass amounts as integer bigint lamports, using parseSol('0.1') to convert user input. A withdrawal amount is the total private-balance deduction, including the send fee.

ActionSDK flow
Deposit SOLprepareDeposit({ lamports }), review it, then call submitDeposit(prepared), which requests wallet approval.
Send privatelyprepareWithdrawal({ lamports, recipient }), review its quote, then submitWithdrawal(prepared).
Show confirmationCheck getPaymentStatus(payment) or use waitForConfirmation(payment). Show success only when the result is confirmed.

Preparation generates a proof without broadcasting. Before submission, privately save the complete payment intent, including its authentication field. Prepared objects belong to the same live client. The convenience methods deposit() and send() also submit; they are not preview methods.

If submission is uncertain or confirmation times out, keep the tracking record and check its status. After a restart, use resumePayment() with the same wallet and signing context, unlock, and resume status checks. Do not automatically submit another payment.

For browser integrations

The Node.js example is not a complete browser setup. Initialize the Poseidon WASM hasher explicitly and pass it as hasher. Make the pinned proving files accessible through createProver({ baseUrl }), using same-origin hosting or suitable CORS. Your RPC must also permit browser access.

The official API restricts third-party browser origins. Configure a controlled backend proxy or compatible API through apiUrl; it must never receive the unlock signature or private witness.

Private balances are bound to the signing host. Browsers use the current page’s host and reject another signingHost. The same wallet on your domain does not automatically access its balance from app.zkpay.sh. Node.js defaults to app.zkpay.sh; preserve the original signing host when recovering a balance.

STEP 1

Deposit SOL.

Before you start

Use OKX Wallet, Solflare, Jupiter, or Backpack. Your wallet needs to support the selected Solana network, message signing, and versioned transactions.

Keep some SOL in your external wallet for transaction fees and account rent.

  1. Open app.zkpay.sh and check the network shown in the top-right corner.
  2. Select Connect Wallet. Approve the connection and carefully review the message-signing request that unlocks your private balance.
  3. Choose Deposit and enter the amount of SOL to move into the pool.
  4. Select Deposit SOL, review the transaction, and approve it in your wallet.
  5. Wait for confirmation before using the deposited balance on Send.
A message signature is sensitive. Unlocking sends no transaction and costs no gas, but the resulting signature can be used to derive your private spending key. Only sign on app.zkpay.sh. Never share the signature.

Wallet balance is not private balance

Wallet shows the SOL still in your external wallet. Available on Send shows your private pool balance. A deposit of 0.1 SOL adds 0.1 SOL to the pool; the external wallet also pays transaction costs and any required account rent.

STEP 2

Send privately.

You can send to a Solana address without the recipient connecting a wallet to zkPay. The payment is made from the pool through the relayer, rather than directly from your depositing wallet.

  1. Choose Send and enter the amount to deduct from your private balance. Max selects the available balance, including fees.
  2. Paste the Recipient address. Check the complete address and make sure the recipient accepts SOL on the selected Solana network.
  3. Review Recipient receives.
  4. Select Send privately. Keep the tab open while your browser generates the proof and the relayer submits the transaction.
  5. Look for Payment sent, then use View transaction to inspect the onchain result.

There is one recipient per payment. Once your balance is unlocked, sending does not normally require another wallet transaction approval. Any unspent change remains in your private balance.

There is no protocol-enforced waiting period after a confirmed deposit. This does not mean an immediate send is unlinkable: timing and similar amounts can reveal a relationship.

Check before retrying. A slow confirmation or an app timeout does not mean the transaction failed. Check the transaction in the explorer and refresh your balance before submitting another payment. Confirmed payments cannot be reversed.

LEARN THE CONCEPTS

What is zero-knowledge?

A zero-knowledge proof lets someone demonstrate that a statement is true without revealing the private information used to prove it.

Think of proving that you know the combination to a safe without telling anyone the combination. The verifier needs evidence that you know the secret, not the secret itself. This is an intuition for the idea, not the exact protocol zkPay uses.

A statement, a witness, and a proof

  • Statement: what is being checked. For a private payment, this includes authorization to spend valid funds without creating money.
  • Witness: the private information that makes the statement true, such as spending-key material and the private notes being spent.
  • Proof: cryptographic evidence the verifier can check without being given that witness.

Some values are deliberately public. A proof can hide the private note behind a payment while still binding that payment to a public recipient and amount. “Zero-knowledge” does not mean “zero public information.”

The three properties

Completeness
An honest prover with a valid witness can produce a proof the verifier accepts.
Soundness
A dishonest prover should not be able to convince the verifier of a false statement, assuming the system’s cryptographic assumptions hold.
Zero-knowledge
The proof should reveal no additional information about the witness beyond what the public statement already reveals.

Zero-knowledge is not encryption

Encryption hides a message so that someone with the right key can read it later. A zero-knowledge proof shows that a computation or statement is valid without revealing its private inputs. A private payment system can use both: encryption to protect note details, and a proof to validate spending.

What does zk-SNARK mean?

Zero-Knowledge Succinct Non-Interactive Argument of Knowledge. “Succinct” means the proof is compact and comparatively cheap to verify. “Non-interactive” means the verifier can check the proof without a back-and-forth conversation with the prover. “Argument of knowledge” refers to the computational guarantee that a successful prover knows an appropriate witness.

zkPay uses a SNARK system called Groth16. Your browser does the proof-generation work; the Solana program verifies the proof before accepting a payment. Not every zero-knowledge system uses Groth16 or requires the same kind of setup.

Further reading: Circom’s official background guide explains circuits, private inputs, and the proof workflow.

CONNECT THE IDEAS

Inside a private payment.

1. Notes, commitments, and encrypted records

A note represents a private claim on SOL in the pool. It is not another coin or token. It contains an amount, a spending public key, and random blinding data.

A commitment is a cryptographic fingerprint of that note. zkPay uses the Poseidon hash function to combine the amount, spending public key, blinding data, and asset identifier. The random blinding matters: simply hashing a predictable amount like 1 SOL would be easy to guess.

The pool records commitments publicly. Separately, encrypted notes let the owner recover the private details and reconstruct a balance. On scanning, the client decrypts a note and checks that it matches the public commitment. Encryption and proof verification perform different jobs.

The deposit itself is still public. A hidden note does not hide which wallet deposited SOL or the deposit amount.

2. Merkle trees: membership without naming the note

A Merkle tree combines many commitments into a compact summary called a root. A membership path connects one commitment to that root. In zkPay, the path and selected note are private inputs to the proof.

The checked-in circuit uses a tree of depth 26. The program checks an accepted root, while the proof establishes that a nonzero input note belongs to that tree without revealing its selected leaf. The commitment history remains public; it is the membership choice that the proof conceals.

3. Nullifiers: preventing a second spend

A nullifier is a public spent marker derived from a note and its private spending information. The proof establishes that it was derived correctly. The program separately rejects a nullifier already used onchain.

These checks work together: zero knowledge alone does not maintain a spent-note database, and membership in a Merkle tree does not mean a note is still unspent. A nullifier is not a refund code or a secret you should enter into a website.

4. Circuits: the rules a proof must satisfy

A circuit expresses a set of mathematical constraints. zkPay’s circuit checks ownership of input notes, valid commitments and nullifiers, membership for nonzero inputs, output amount ranges, and conservation of value.

A simplified way to read the conservation rule is: value entering a transaction must equal value leaving it. A withdrawal consumes private notes, pays a recipient and fee, and creates private change if needed.

Example: 1 SOL private balance, 0.4 SOL Send
Recipient: 0.3932 SOL
Fee: 0.0068 SOL
Private change: 0.6 SOL

The circuit has two input slots and two output slots. Unused slots can be filled with zero-value dummy notes. This does not mean the app sends to two recipients: the current Send flow has one external recipient.

5. Public inputs: binding the payment

The proof has seven public scalar inputs: the Merkle root, public amount, external-data hash, two input nullifiers, and two output commitments.

The external-data hash binds the recipient, amount, fee, fee recipient, and encrypted output bytes to the proof. The contract recomputes it before accepting a payment. A relayer cannot simply swap the destination while keeping an unchanged valid proof.

A hash here authenticates the relationship between fields; it does not make those fields secret. The recipient, external amount, and fees are still supplied to the program and visible onchain.

6. Local proving and onchain verification

Your browser computes the private witness and Groth16 proof. The relayer receives the proof and payment data, then submits a Solana transaction. The program verifies the proof, checks the root, rejects spent nullifiers, enforces fees, and processes the payment.

The proving key downloaded by the app is a public cryptographic parameter file, not your wallet private key. Downloading it does not let someone spend your funds. Your private witness and spending key must still stay secret.

7. Trusted setup: a separate security assumption

Groth16 uses proving and verification parameters created through a setup ceremony. Secret randomness from that ceremony must not be retained in a way that enables forged proofs. Security depends on correct ceremony execution and at least one honest secret contribution whose randomness was destroyed.

Keep learning

  • Circom: proving circuits — how setup, proving, and verification fit together.
  • Zcash Protocol Specification — a deeper reference for shielded notes, commitments, and nullifiers. Zcash and zkPay are different protocols; their features and security properties are not interchangeable.

BEFORE USING REAL SOL

Security & recovery.

Protect your wallet and unlock signature

Your wallet signature is used to derive your private spending key. Anyone who obtains that signature may be able to spend the corresponding private funds. Never share it, your seed phrase, or your private key with a website, support contact, or chat.

Check app.zkpay.sh in the address bar before signing. A malicious site can copy the legitimate signing message; a familiar-looking message is not proof that a site is trustworthy.

Recover access to your private balance

Reconnect the same wallet account on the official app, choose the same network, and sign the same unlock message. The app derives the key again and scans the pool’s encrypted notes to reconstruct the available balance.

This depends on retaining access to the original wallet key and compatible message signing. A different wallet account, network, pool, or signing behavior can produce a different key. There is no zkPay password reset, and zkPay cannot recover a lost wallet key for you.

VERIFY WHERE YOU ARE

Networks & contracts.

NetworkAssetApp
Mainnet BetaReal SOLOpen Mainnet
DevnetTest SOL, no monetary valueOpen Devnet

The pools and private balances are separate. Changing the network does not move funds between them. zkPay currently supports SOL, not SPL tokens, USDC, or cross-chain transfers.

Mainnet program
98Bj9K8iPV1JiVqBWXzY4bX4wsrm2x5DgEbiToybm9hx
Devnet program
79EUG9jBTvcLenrTTYaHBzX6dqM9osaUXhLs3hVf4vBk

Use the deposit flow in the app. Do not send SOL directly to a program address expecting it to credit your private balance.

KNOW THE AMOUNT

Fees & amounts.

Deposits have no fee. Your wallet separately pays the Solana transaction fee and any required account rent.

Each Send currently costs 0.2% of the entered amount + 0.006 SOL. The fee comes out of that amount; it is not added on top. The relayer submits the payment and pays its network transaction costs.

Recipient receives = Entered amount − Send fee
Send fee = 0.2% × Entered amount + 0.006 SOL

Amounts are calculated in lamports: 1 SOL = 1,000,000,000 lamports. The exact fee is floor(amount × 20 ÷ 10,000) + 6,000,000 lamports.

WHEN SOMETHING LOOKS WRONG

Troubleshooting.

My wallet is missing or cannot sign.

Use a supported wallet: OKX Wallet, Solflare, Jupiter, or Backpack. Unlock the extension, check the selected network, and allow it to connect to the official app. The wallet must support message signing and versioned Solana transactions. Do not paste a private key into the app as a workaround.

I deposited SOL, but Wallet shows less.

Wallet is your external wallet balance, not your private balance. The deposited SOL has moved into the pool, and your wallet also paid transaction costs and possibly account rent. Check Available on Send after confirmation.

I reconnected and my private balance is zero.

Check the wallet account, Mainnet versus Devnet, and the exact official hostname. Allow the balance scan to finish. A different account or network derives a different key. If the original deposit was pending, inspect its transaction first. Do not make a second deposit just to try to restore access.

The recipient received less than I entered.

The entered amount is the total deduction from your private balance. The recipient receives it minus the send fee, currently 0.2% + 0.006 SOL. For example, entering 1 SOL sends 0.992 SOL. See the fee details.

Proof generation or confirmation is taking a long time.

Keep the tab open. Proof generation depends on your device, while submission and confirmation depend on the relayer and network. A timeout is not proof of failure. Check any available transaction link and refresh the balance before trying again. Never share the unlock signature when describing an error.

Can a payment be cancelled or recovered?

A confirmed Solana payment cannot be reversed by zkPay. If you sent to the wrong address, zkPay cannot take the funds back. Check the complete recipient address before sending.

Do I need to wait after depositing?

There is no protocol-enforced waiting period after confirmation. Waiting alone does not guarantee privacy; timing, amounts, a small pool, and address reuse can still allow correlation.