Core APIs
transact(params, options)transfer(...)partialWithdraw(...)fullWithdraw(...)swapUtxo(...)swapWithChange(...)
@cloak.dev/sdk.
Transaction semantics
externalAmount rules:
> 0: public deposit into the pool< 0: public withdrawal from the pool0: fully shielded transfer
264-byte public inputs and submits the resulting on-chain transaction.
Fees
- SOL withdraw/swap:
gross = abs(externalAmount)fee = 5_000_000 + floor(gross * 3 / 1000)net = gross - fee
- SPL (USDC/USDT) withdraw:
fee = 450_000 + floor(gross * 3 / 1000)in token base units, i.e. 0.45 token fixed plus 0.3%, charged in the withdrawn token - Deposits and shielded transfers have no protocol fee, but every deposit must meet the
on-chain minimum:
10_000_000lamports (0.01 SOL) for the SOL pool, and1_000_000base units (1.00 token) for the 6-decimal USDC/USDT pools. Anything below the minimum is rejected by the program withDepositTooSmall(0x1038). The SDK exportsMIN_DEPOSIT_LAMPORTSfor the SOL pool only, so an under-minimum USDC or USDT deposit is not caught client side and fails at submission.
- SOL:
calculateFeeBigint(gross)for the fee,gross - calculateFeeBigint(gross)for the net,isWithdrawAmountSufficient(gross)to check the withdrawal clears the fee.
Create UTXOs
Every result carries notes you must persist
transact, transfer, partialWithdraw, swapUtxo and swapWithChange all resolve to a result
whose outputUtxos hold your money: the note a deposit created, and the change note left over by a
spend. fullWithdraw is exempt only when you hand it one or two notes, because only then does it
leave nothing shielded. Given three or more notes it merges them on chain first, and that path
strands money on failure:
withdrawing three or more notes consolidates first.
serializeUtxo and deserializeUtxo are the persistence primitives. Every sample on this page
calls the same helper rather than repeating it:
transact pads its outputs to a fixed count with zero-amount notes, and
transfer builds the recipient’s output from the recipient’s public key alone, so that note comes
back with privateKey set to 0n and is not yours to hold.
Where those bytes should live, how to reload them, and why UtxoWallet.serialize() drops
commitment so that verifyUtxos then reports those notes as skipped:
where your notes live between sessions.
Withdrawing three or more notes consolidates first
fullWithdraw is a thin wrapper: it sums your inputs and calls partialWithdraw for the whole
amount. partialWithdraw can spend only two notes at a time, because the circuit takes two inputs
and two outputs. So when either call receives three or more notes, it first merges the two smallest
on chain, repeatedly, until two are left, and only then withdraws.
Each merge round is a real shield-to-shield transaction. It nullifies the two notes it consumed and
creates one merged note whose blinding comes from randomFieldElement() inside createUtxo, the
same fresh randomness as any other note. That merged note exists only in a local variable inside the
call until the next round consumes it.
With one or two input notes there is no loop and no exposure. fullWithdraw then genuinely leaves
nothing shielded, and its outputs are padding rather than money.
onProgress is the only signal a caller gets that consolidation is happening at all. It fires once
per merge round with Consolidating notes (n remaining)..., so a call that emits it has already
committed to multi-round on-chain work:
transact with externalAmount: 0n, and it hands the merged note back to you so you
can write it down before the next one:
Choosing a signer
Every example below comes in two tabs. Which one you need is decided by where your code runs, not by which call you are making.- Wallet adapter (browser): you hold no secret key, so you pass the end user’s
walletPublicKeytogether with the adapter’ssignMessage. That pair does two jobs: it signs the viewing-key registration message, which is enforced by default, and it produces the authenticated sender that every request submitted on your behalf carries. A deposit additionally needssignTransaction, because a deposit is signed by the funding wallet and posted straight to the chain. - Keypair bytes (Node, server, CLI):
depositorKeypaircovers all of it and never prompts.
signTransaction alone and carries no
authenticated sender, so the deposit below can succeed while the very next call, the private send,
is the first one that needs signMessage and walletPublicKey. Leave either of them out on a
transfer, a withdrawal or a swap and the call throws before it generates a proof.
Request authentication is the full contract for that signed sender:
the exact options, the 300-second approval window, how many wallet prompts each flow costs, and
explainRelayAuthRejection for turning a rejection into something a user can act on.Deposit example (transact)
relayUrl is the exported CLOAK_PRODUCTION_RELAY_URL. The SDK reads no environment variable for
it, and the published build accepts only an origin on its allowlist, so any other value throws
locally before a request leaves the process. CLOAK_RELAY_URL is a convention the SDK’s own
examples and scripts read themselves and pass in as relayUrl. See
which endpoint the SDK talks to.Withdraw and transfer helpers
These three are submitted on your behalf, so each request carries an authenticated sender. In the browser that is whatwalletPublicKey and signMessage are for, and there is no signTransaction
in sight because you are not signing a transaction here. The key you pass becomes the request’s
sender, so it must be the end user’s own wallet, never a session or service key.
partialWithdraw/fullWithdraw use negative externalAmount semantics.
Each of these asks the wallet to sign exactly once, whatever happens on the network: a stale-root
retry re-uses the same proof and the same signed request. See
how many wallet prompts to expect.
Swap example
swapWithChange sends a TransactSwap payload (proof + 264-byte public inputs + swap params).
A swap returns two things worth keeping, not one. outputUtxos carries the change note, exactly as
every other flow does. refund is separate: it is the secret that reclaims the swap principal if
the swap times out before it executes, and swapStatePda is the swap it belongs to. Persisting
refund is always the fast path. When you passed chainNoteViewingKeyNk, the pair
discoverSwapRefunds / matchSwapRefundLeaf can rebuild the same authorization from your nk and
the swap’s first input nullifier, so a lost copy is recoverable. When no nk was supplied, the
returned object is the only copy that will ever exist, and persisting it is the only path.
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 needing a fresh approval. maxWalletApprovals caps that at 5
by default, wallet-adapter path only. A depositorKeypair never prompts. Details in
request authentication.
Viewing-key registration requirements
- Default behavior enforces viewing-key registration before protocol txs.
TransactOptions.enforceViewingKeyRegistrationdefaults totrue.- Registration signs the fixed sign-in message and submits the registration record.
Risk-oracle deposits
When deposits require Range/Switchboard validation, include:riskOracleQueueriskQuoteUrl(orgetRiskQuoteInstruction)
addressLookupTableAccounts.
Operational notes
- Every call hands its new notes back on
result.outputUtxosand persists none of them. Write them out withserializeUtxobefore you report success.fullWithdrawis exempt only for one or two input notes, where it leaves nothing shielded and its outputs are padding rather than money. See every result carries notes you must persist. partialWithdrawandfullWithdrawmerge on chain before withdrawing whenever they are given three or more input notes, and a failure mid-merge strands a note that no result ever hands back.onProgressis the only signal. See withdrawing three or more notes consolidates first.- Amounts use
bigintin the UTXO API. - Proof retries are built in for stale roots (
0x1001). - SDK-side Merkle reconstruction is preferred for older indices.
- Optional
cachedMerkleTreehelps sequential txs avoid extra rebuild/fetch. useUniqueNullifiersdefaults totrue: every padding UTXO gets a random salt, so padding nullifiers never collide between runs or between concurrent depositors. Leave it unset. Setting it tofalserestores deterministic padding salts, which collide and fail withDoubleSpend(0x1020) once more than one deposit shares them.
Troubleshooting
Invalid public inputs size: the protocol expects264bytes, not232.RootNotFound/0x1001: regenerate proof with fresh root (SDK retry usually handles this).0x1020(double spend): the input UTXO was already spent, usually from another session or device.transactandswapUtxopre-flight the input nullifiers and throwUtxoAlreadySpentErrorbefore any proof is generated. Catch it and callverifyUtxos()to reconcile your local notes with chain state.viewing key not found: re-run a transaction with registration enabled, then rescan history.Unauthorized, a bare401, orrequires an authenticated senderon a transfer, withdrawal or swap: the call had no usable signer, or only half of one. In the browser passsignMessageandwalletPublicKey; on a server passdepositorKeypair. Full diagnosis in Request authentication.
Next
- Storing the notes these calls return: Wallet integration
- Signing requests: Request authentication
- Protocol behavior: Shield Pool Program
- Runtime lifecycle: Transaction Flows
- Full SDK exports: API Reference