zkAPIDocs

THE ESSENTIALS

A guide to zkAPI.

zkAPI is privacy infrastructure on Robinhood Chain. Deposit ETH into a private balance, then send to a fresh address with a proof generated in your browser.

The proof lets you spend without revealing which private notes you are spending. The recipient and the amount leaving the pool remain public.

YOUR FIRST PAYMENT

Two steps, one private balance.

  1. Deposit ETH

    Connect a supported wallet, unlock your private balance with two matching message signatures, then deposit ETH. Confirm the deposit transaction in your wallet.

  2. Send privately

    Enter a recipient’s Robinhood Chain 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 an address that accepts ETH on Robinhood Chain Mainnet. They do not need to connect to zkAPI.

Open the app

DEVELOPER QUICK GUIDE

SDK integration.

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

Supports native ETH on Robinhood Chain Mainnet (4663). The public repository contains the SDK, configuration, examples, and integration guides.

1. Build from source

Use Node.js 22 or newer. The SDK is a source release; it is not published on npm.

git clone https://github.com/0xzkAPI/zkapi-sdk.git
cd zkapi-sdk
npm ci
npm run check
npm pack

This checks the types, runs offline tests, builds the SDK, and creates a package archive. Install the archive in your application:

npm install /absolute/path/to/zkapi-sdk/zkapi-robinhood-sdk-0.3.1.tgz

2. Supply a wallet and RPC

  • Wallet: pass the user’s selected Eip1193Provider. The account must be an externally owned account (EOA) with deterministic personal_sign support. Wallet selection and approval screens belong to your application.
  • RPC is required: explicitly pass your own rpcUrl for Robinhood Chain Mainnet. The SDK has no built-in or fallback RPC.
  • Browser access: your RPC must allow requests from your application. Use HTTPS, keep server credentials out of browser code, and avoid logging provider URLs containing API tokens.

Connected chain reads use the selected wallet provider. Wallet-free receipt recovery uses your configured HTTP RPC; network registration also receives your RPC URL. The separate apiUrl selects the indexer and relay service. Supplying an RPC does not replace those services.

3. Unlock and read a balance

Call this helper after the user selects a wallet and chooses to unlock. It can request a network switch and two recovery signatures. It does not submit a payment.

import {
  createZkApiClient,
  createRobinhoodMainnetConfig,
  formatEtherExact,
  LEGACY_RECOVERY_ORIGIN,
} from '@zkapi/robinhood-sdk';
import type { Eip1193Provider } from '@zkapi/robinhood-sdk';

export async function readBalance(
  provider: Eip1193Provider,
  rpcUrl: string,
) {
  const client = createZkApiClient(createRobinhoodMainnetConfig({
    rpcUrl,
    origin: LEGACY_RECOVERY_ORIGIN,
  }));
  try {
    await client.connect(provider, { switchChain: true });
    const balance = await client.unlock();
    return {
      totalEth: formatEtherExact(balance.privateBalanceWei),
      spendableEth: formatEtherExact(balance.maxSpendableWei),
    };
  } finally {
    client.disconnect();
  }
}

Keep the client connected for a payment flow; this read-only helper disconnects when finished. The circuit can spend at most two notes per payment, so the amount spendable in one payment can be lower than the total balance.

4. Add Deposit and Send

Use a connected, unlocked client. Pass amounts as integer bigint wei; use parseEtherExact('0.1') to convert user input without floating-point rounding.

ActionSDK flow
Deposit ETHdeposit(amountWei) generates a proof, asks for wallet transaction approval, and submits the deposit.
Send privatelyRefresh with sync(), review quoteSend(grossWei), then call send({ grossWei, recipient, expectedFeeBps }).
Send MaxsendMax(recipient, { expectedFeeBps }) submits the maximum spendable in one two-note proof, with fees deducted.
Show confirmationShow success only when the result has status: 'confirmed'. A pending result is still unresolved.

Deposit, Send, and Send Max submit payments. Call them only after review and an explicit user action. Bind the reviewed fee with expectedFeeBps; ask for a new review if the fee changes.

5. Resume an uncertain payment

Save public transaction hashes and request IDs from onProgress as soon as they become available. Keep tracking records separated by network and pool. Never save recovery signatures, keys, private notes, or witnesses.

When a hash is available, use waitForConfirmation(hash, { requestId }). For a Send with only a request ID, use retryRelay(requestId) to check the same payment. A timeout, 404, or RPC error is not proof of failure; do not automatically create a replacement payment.

A request ID cannot reconstruct a proof lost before reaching the relay. Keep controls blocked while the outcome is unresolved. If the transaction confirms before the indexer catches up, show the confirmed transaction and wait for the balance to refresh.

For browser integrations

Use an ESM browser bundler with Web Crypto, fetch, BigInt, and an HTTPS or localhost origin. Proof generation runs locally; no separate Poseidon initialization is required.

The Mainnet helper pins the pool and proof-file hashes. It defaults to https://app.zkapi.org/api/hood for the indexer and relayer. Configure apiUrl and artifactBaseUrl for your application’s browser access; third-party origins may need a controlled API proxy and a static proof-file host with suitable CORS.

From the SDK source checkout, npm run artifacts downloads and verifies the three public proof files into .artifacts/. Serve those files unchanged. The proving key is a public parameter file, not a wallet secret.

Preserve the recovery identity. The official app retains https://app.zkpay.sh and the original zkPay signing text for compatibility. Use LEGACY_RECOVERY_ORIGIN intentionally to restore that balance. A new integration can choose its own stable origin for a separate balance. Changing these cryptographic strings changes the recovery key.

STEP 1

Deposit ETH.

Before you start

The picker shows detected EVM wallets and prioritizes OKX Wallet, Phantom, MetaMask, and Uniswap Wallet. Use an installed EVM wallet that supports Robinhood Chain, deterministic message signing, and an externally owned account. Smart-contract and delegated accounts are not supported for unlocking a balance.

Keep some ETH in your external wallet for the deposit transaction’s gas fee.

  1. Open app.zkapi.org and connect your preferred wallet.
  2. Switch to Robinhood Chain Mainnet if prompted. Review and sign the two recovery messages to unlock your private balance.
  3. Choose Deposit and enter the amount of ETH to move into the pool.
  4. Review the deposit and approve the transaction in your wallet after the browser finishes generating the proof.
  5. Wait for confirmation and the private balance to refresh before sending.
A message signature is sensitive. Unlocking sends no transaction and costs no gas, but the signature derives your private spending key. Check app.zkapi.org in the address bar before signing. Never share the signature.

Wallet balance is not private balance

Your wallet balance is the ETH still in your external wallet. Your private balance is the ETH held in pool notes you can spend. Depositing 0.1 ETH adds 0.1 ETH to that private balance; your external wallet separately pays the network gas fee.

STEP 2

Send privately.

Send ETH to an address on Robinhood Chain without the recipient connecting to zkAPI. The payment comes from the pool through the relayer, rather than directly from your depositing wallet.

  1. Choose Send and enter the total amount to deduct from your private balance, including fees.
  2. Paste the recipient address. Check the complete address and confirm that the recipient accepts ETH on Robinhood Chain Mainnet.
  3. Review the total amount deducted and Recipient receives.
  4. Select Send privately. Keep the tab open while your browser generates the proof and the relayer submits the transaction.
  5. Wait for confirmation, then open the transaction link to inspect the onchain result.

Max fills in the amount spendable in one payment. A proof can consume at most two notes, so Max can be lower than your total private balance. Selecting Max does not send the payment.

There is one recipient per payment. Once your balance is unlocked, sending does not normally require another wallet transaction approval. The relayer pays the network gas fee, and unspent change remains in your private balance.

A fresh recipient address helps avoid address reuse; zkAPI does not check whether an address has been used before. There is no protocol-enforced waiting period after a confirmed deposit, but timing and similar amounts can reveal a relationship.

Check before retrying. A slow confirmation or an app timeout does not mean the transaction failed. Keep the original transaction or request ID and check its status. 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 zkAPI 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.

zkAPI uses a SNARK system called Groth16. Your browser does the proof-generation work; the pool contract 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 ETH 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. zkAPI uses the Poseidon hash function: Poseidon(2, poolDomain, amount, publicKey, blinding). The pool domain binds notes to their deployment context. The random blinding matters: simply hashing a predictable amount like 1 ETH 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 ETH 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 zkAPI, the path and selected note are private inputs to the proof.

The circuit uses a tree of depth 26. The contract 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 contract 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. zkAPI’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 at 20 basis points: 1 ETH private balance, 0.4 ETH Send
Recipient: 0.39865 ETH
Fee: 0.00135 ETH
Private change: 0.6 ETH

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 eight public scalar inputs: the pool domain, 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 contract 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 transaction on Robinhood Chain. The contract 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 zkAPI are different protocols; their features and security properties are not interchangeable.

KEEP ACCESS TO YOUR BALANCE

Security & recovery.

Protect your wallet and recovery signature

Your wallet signature derives 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.zkapi.org in the address bar before signing. A malicious site can copy the legitimate signing message; familiar wording alone does not make a site trustworthy.

Recover access to your private balance

Reconnect the same wallet account on the official app, on Robinhood Chain Mainnet, and sign the same two recovery messages. The app derives the key again and scans the pool’s encrypted notes locally to reconstruct the available balance.

Recovery depends on the original wallet key, deterministic signatures, network, pool, and recovery origin. The app checks that both signatures match after normalization. A different account or signing behavior can produce a different key. zkAPI has no password reset and cannot recover a lost wallet key.

The signing message retains the original zkPay identity for existing balances. This is expected on the official app; developers should preserve LEGACY_RECOVERY_ORIGIN when intentionally accessing the same balance.

Understand what remains public

Depositing wallets, amounts entering and leaving the pool, recipients, fees, and transaction times are visible onchain. Commitments, nullifiers, and encrypted note records are also public. The proof hides the private witness and which notes are consumed.

Address reuse, distinctive amounts, timing, and a small pool can allow correlation. RPC, relay, and hosting services may also observe IP addresses and request timing. Using a fresh address does not guarantee anonymity.

VERIFY WHERE YOU ARE

Network & contracts.

NetworkChain IDAsset
Robinhood Chain Mainnet4663 (0x1237)Native ETH

zkAPI supports native ETH on Robinhood Chain Mainnet. It does not bridge funds from another chain or accept ERC-20 token deposits.

Pool contract
0xF3A2D484d909C48C8581B99750B28D5F9af3E1E6
Proof verifier
0x952F13f3c6a41B291f250EF8D0b56986E1F89da0
Relayer
0x0D7fd3755ca7122db807B21BAc09Bc327a9A7289

Use the app’s Deposit flow. Sending ETH directly to a contract address does not create the encrypted notes needed to credit and recover a private balance.

KNOW THE AMOUNT

Fees & amounts.

Deposits have no platform fee. Your wallet separately pays the Robinhood Chain transaction gas fee.

Each Send deducts a percentage fee plus a fixed 0.00055 ETH from the entered 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 = floor(grossWei × feeBps ÷ 10,000) + 550,000,000,000,000 wei

The percentage is configurable. Review the quote shown in the app before sending; SDK integrations should load fresh state and use client.quoteSend(grossWei).

WHEN SOMETHING LOOKS WRONG

Troubleshooting.

My wallet is missing or cannot sign.

Unlock the wallet extension and allow it to connect to app.zkapi.org. The picker prioritizes OKX Wallet, Phantom, MetaMask, and Uniswap Wallet. Use the EVM account on Robinhood Chain Mainnet; the account must support deterministic personal_sign. Smart-contract and delegated accounts cannot unlock a balance. Never paste a private key into the app.

Why am I asked to sign twice?

The app requests the same recovery message twice to verify that your wallet returns consistent signatures. These signatures unlock your private balance; they do not transfer ETH or cost gas. If you switch accounts or networks, reconnect and unlock the intended account.

I deposited ETH, but my wallet shows less.

The deposited ETH moved into your private balance, and your wallet also paid the deposit gas fee. Your external wallet balance and private pool balance are different. Wait for confirmation and the balance scan to finish.

I reconnected and my private balance is zero.

Check the original wallet account, Robinhood Chain Mainnet, pool, and recovery identity. Allow the scan to finish. If the deposit was pending, inspect its original transaction first. Do not make another deposit to try to restore access. The official app preserves the original recovery identity across the brand change.

Why does Max show less than my total balance?

One proof can spend at most two notes. If your balance is spread across more notes, Max selects the amount currently spendable in one payment. Fees are included in that amount. Your remaining notes stay in the private balance.

The recipient received less than I entered.

The entered amount is the total deduction from your private balance, including the send fee. For example, at 20 basis points, entering 0.1 ETH sends 0.09925 ETH. Review Fees & amounts and the quote before confirming.

Proof generation or confirmation is taking a long time.

Keep the tab open. Proof generation depends on your device; submission and confirmation depend on the relayer and network. A timeout is not proof of failure. Check the original transaction link or request ID. A confirmed transaction can appear before the indexer refreshes your balance; do not send again because the balance is stale.

Can a payment be cancelled or recovered?

A confirmed payment cannot be reversed by zkAPI. If you sent to the wrong address, zkAPI cannot take the funds back. Verify the entire recipient address and the receiving network 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.