Skip to main content
Use this page when you want the smallest possible end-to-end Cloak integration.
This snippet is the server-side path. It uses depositorKeypair, which a browser wallet adapter cannot supply. Deposits work either way, but the withdrawal below is submitted for you and needs an authenticated sender, so in a browser you pass signMessage plus walletPublicKey instead. Read Request authentication before you port this to a frontend; skipping it is what produces a 401 on the first send after a deposit that looked fine.
This snippet covers:
  • shielded SOL deposit
  • full withdrawal (send)
  • partial withdrawal (keep change private)
Every call that leaves value shielded returns outputUtxos, and those notes are your own money. Nothing on chain carries a note’s blinding, so a note you drop cannot be rescanned or recovered by anyone, including us. That is why the snippet writes down the deposit note as well as the change note, even though it spends the deposit a few lines later: the deposit is confirmed on chain before the withdrawal runs, so a withdrawal that throws would leave that SOL shielded with only a dead process able to spend it. Same run is not the same as one atomic step. serializeUtxo gives you the 128-byte encoding that deserializeUtxo turns back into a spendable note, and files are just the simplest possible sink for it. Pick a real one before you have users: where your notes live between sessions.
For minimal script generation, keep this exact interface:
  • file: send-sol-private.ts
  • command: npx tsx send-sol-private.ts <recipientPubkey> <lamports>
  • env vars your script reads: SOLANA_RPC_URL, KEYPAIR_PATH, optional CLOAK_RELAY_URL
Guardrails:
  • keep tx amounts as bigint
  • use KEYPAIR_PATH (file-based keypair), not raw private key env vars
  • pass relayUrl explicitly on every call. The SDK reads no environment variable for it, so CLOAK_RELAY_URL is purely a convention for your own script: your code reads it and passes the value through. The only value this build accepts is CLOAK_PRODUCTION_RELAY_URL (https://api.cloak.ag), so treat the env var as a label, not a switch, and prefer importing the constant
  • do not parse SOL decimals with float math (parseFloat, AMOUNT_SOL)
  • read recipient/amount from process.argv (<recipientPubkey> <lamports>), not RECIPIENT_ADDRESS/SEND_LAMPORTS env vars
  • keep programId fixed to CLOAK_PROGRAM_ID internally
  • rely on SDK stale-root retries for standard flows
  • call process.exit(0) on success and process.exit(1) on failure

Mainnet smoke test (keypair bytes)

Use this when you want a first real send with explicit runtime values.
Sanity-check before running:
Run your script entrypoint that wraps the snippet above:

Funding and net amount

For SOL withdraw/send paths:
  • gross = abs(externalAmount)
  • fee = 5_000_000 + floor(gross * 3 / 1000)
  • net = gross - fee
Worked example (gross = 50_000_000, i.e. 0.05 SOL):
  • fee = 5_150_000 lamports (0.00515 SOL)
  • net = 44_850_000 lamports (0.04485 SOL)
Recommended first-run sender balance for this smoke test: 0.07 SOL or higher. Next: Code Examples, then where your notes live between sessions for a real home for the change notes this snippet writes to files.