Skip to main content
All examples use the current default program ID:
  • zh1eLd6rSphLejbFfJEneUwzHRfMKxgzrgkfwA6qRkW

Pick your path before you copy anything

Everything under Shared setup is the server keypair path. It passes depositorKeypair, which a browser wallet adapter cannot supply: an adapter holds no secret key. Deposits work on either path, but every other flow here is submitted for you and carries an authenticated sender, so from a browser you pass signMessage plus walletPublicKey instead. Copying depositorKeypair into a frontend is what produces a 401 on the first send after a deposit that looked fine. Read Request authentication first, or start from the wallet-adapter version of the send flow below.
Nothing else differs. The parameters, the amounts, the helpers and the results are identical on both paths; only the holder of the pen changes.

Request authentication

Which flows need a signed sender, the 300-second approval window, how many wallet prompts to expect, and what to do when a request is rejected.

Shared setup

relayUrl is a value you pass on every call, and it is not read from the environment. The SDK reads no environment variable for it, so exporting CLOAK_RELAY_URL does nothing on its own. If you like that convention, read the variable in your own code and pass the value as relayUrl; the value still has to be an origin on this build’s allowlist, which is exactly what the exported CLOAK_PRODUCTION_RELAY_URL is. Omitting relayUrl, or passing "", throws Viewing key registration is mandatory: relayUrl is required. before any network call, on every flow including deposits. "" only means “I will submit this myself” when enforceViewingKeyRegistration is also set to false, because the viewing-key gate runs first and fires either way. See the warning in Cloak SDK before you reach for that pair: it is a privacy hazard on a transfer or a withdrawal, not a configuration shortcut.
Write result.outputUtxos down before you report success. Every call on this page returns the notes it just created and then forgets them. A change note is your own money coming back, and its blinding is fresh randomness that createUtxo generates in memory and publishes nowhere, on chain or off. A note you did not store is unspendable by anyone, permanently: no rescan, no seed phrase and no support path brings it back. That is why every example below captures its result and calls saveNotes at the point where your service would commit to its own database. The browser version of saveNotes is in the wallet-adapter example below; for the two storage exports the SDK ships, read where your notes live between sessions.

Send flow (SOL->SOL, USDC->USDC, USDT->USDT)

The same send from a browser wallet adapter

Same functions, same arguments, same results. The only change is the two signing options: the deposit is signed by signTransaction and submitted directly by the wallet, and the withdrawal is authenticated by signMessage over the request, with walletPublicKey as the sender.
partialWithdraw and swapWithChange take the same walletOptions object; swap it in wherever the server examples spread baseOptions. Both of them do return a change note, so the saveNotes call above stops being a no-op the moment you substitute one, and swapWithChange needs its refund stored under a second key as well, for the reason given in the swap section below.
walletPublicKey and depositorPublicKey are the same end user here, which is why both are set to publicKey. Passing two different keys throws: the SDK uses one key for the request’s sender, for screening and for viewing-key registration, so splitting it would authenticate one person and screen another.

SOL swap flow (SOL -> token)

result.refund is the other secret a swap produces, and dropping it is how a timed-out swap becomes unrecoverable. If the swap does not settle, the program locks the principal behind an amount-bound refund note, and the values in refund are what spend it. Because baseOptions passes chainNoteViewingKeyNk, the secret here is derived from your nk and comes back with derivedFromNk: true, so matchSwapRefundLeaf can rebuild it from that key plus the swap’s first input nullifier. Storing it is still the fast path, and when no nk was supplied the returned object is the only copy that will ever exist. Field-by-field detail is in the API reference.
Swaps are the one flow that can prompt more than once. A swap re-proves on every stale-root retry, and each new proof is a new request that needs a fresh approval, so on the wallet-adapter path add maxWalletApprovals (default 5, floored at 1) to cap how many dialogs one swap may raise. A depositorKeypair never prompts. Sends and withdrawals sign exactly once on either path, whatever happens on the network.

Payroll flow (multi-recipient private payouts)

A loop like this belongs on the server path. Each iteration is a deposit plus a submitted send, so on a wallet adapter it is two approval dialogs per recipient, in sequence, and each request has to be approved inside its window: the user has 285 seconds from the moment they are asked, not from the moment the run started. A payroll of any size run from a browser tab will strand itself on an unattended prompt.

History + compliance scan

Request authentication

The signed-sender contract for every submitted flow, on both paths.

Wallet integration

Where your notes live between sessions, plus the full wallet-adapter wiring, progress callbacks and stale-root retries.

Error handling

CloakError categories, parseError, and the stale-root retry template.

API reference

The full exported surface, including every symbol on this page.