> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cloak.ag/llms.txt
> Use this file to discover all available pages before exploring further.

# Request authentication

> Private sends, withdrawals and swaps are submitted for you and every one carries a signed sender. Wire it up from a browser wallet adapter or from a server keypair.

Deposits go straight to the chain, signed by the wallet that funds them. Every other flow is
submitted on your behalf, and each of those requests carries an **authenticated sender**: a short
ed25519 signature over the exact request body, produced by the end user's own wallet.

This page is the whole contract. Read it before your first private send, because the deposit
example you probably ran first does not exercise any of it.

<Note>
  If you are in a browser and your call fails with `Unauthorized` or a bare `401`, or throws
  `requires an authenticated sender`, you are on the right page. Jump to
  [Wiring it up](#wiring-it-up).
</Note>

## Which flows need it

| Flow                                | `externalAmount` | How it reaches the chain                              | Authenticated sender |
| ----------------------------------- | ---------------- | ----------------------------------------------------- | -------------------- |
| Deposit (shield)                    | `> 0n`           | your wallet signs the transaction, submitted directly | **No**               |
| Private send (shield to shield)     | `0n`             | submitted for you                                     | **Yes**              |
| Partial or full withdrawal          | `< 0n`           | submitted for you                                     | **Yes**              |
| Swap (`swapUtxo`, `swapWithChange`) | `< 0n`           | submitted for you                                     | **Yes**              |

<Warning>
  **Deposits are why this bites people late.** A deposit is signed by the funding wallet and posted
  directly, so it works with `signTransaction` alone and never touches request authentication. The
  [Quickstart](/sdk/quickstart) deposit succeeds, the integration feels wired up, and then the very
  next call, the send, is the first one that needs a signed sender. Nothing was misconfigured at
  deposit time; that step simply does not use this mechanism.
</Warning>

## Wiring it up

You supply one of two things in `TransactOptions`. The SDK builds and signs the request either way.

| Caller                  | Pass                                                              | Prompts the user      |
| ----------------------- | ----------------------------------------------------------------- | --------------------- |
| Browser, wallet adapter | `signMessage` **and** `walletPublicKey` (or `depositorPublicKey`) | yes, once per request |
| Node, server, CLI       | `depositorKeypair`                                                | no                    |

<Warning>
  **One rule applies to every sample below.** Authentication decides whether a request is accepted;
  it does nothing about what comes back. `transfer`, `transact`, `partialWithdraw`, `swapUtxo` and
  `swapWithChange` all hand you `result.outputUtxos` once and then forget them, and one of those
  notes is your user's own **change**. Its `blinding` is fresh randomness drawn inside `createUtxo`
  and written nowhere on chain, so a change note that is not in durable storage before you report
  success is permanently unspendable by anyone: no rescan, no seed phrase, no support path. Persist
  with `serializeUtxo`, read back with `deserializeUtxo`. `fullWithdraw` is the one exemption, and only
  when it spends **one or two** notes: handed three or more it merges them on chain first, and a
  failure part way through loses the merged value with no result to persist. Full treatment:
  [where your notes live between sessions](/sdk/wallet-integration#where-your-notes-live-between-sessions)
  and
  [a withdrawal of three or more notes is not exempt](/sdk/wallet-integration#a-withdrawal-of-three-or-more-notes-is-not-exempt).
</Warning>

### Browser: `useWallet()`

This is the complete browser path. It holds no secret key, and it is what a wallet adapter can
actually supply.

```tsx theme={null}
import {
  CLOAK_PROGRAM_ID,
  CLOAK_PRODUCTION_RELAY_URL,
  explainRelayAuthRejection,
  serializeUtxo,
  transfer,
  addressFromPublicKey,
  type CloakRpc,
  type Utxo,
} from "@cloak.dev/sdk";
import { useWallet } from "@solana/wallet-adapter-react";

// `persistNotes` is your own storage, not the SDK's: it has none. It must have
// committed the bytes before it resolves.
export function usePrivateSend(
  connection: CloakRpc, // from createCloakRpc(rpcUrl)
  persistNotes: (noteBytes: Uint8Array[]) => Promise<void>,
) {
  const { publicKey, signMessage } = useWallet();

  // `recipientPubkey` is the recipient's Cloak pubkey, not a Solana wallet address.
  return async function send(notes: Utxo[], recipientPubkey: bigint, amount: bigint) {
    // Not every adapter implements signMessage. Check before you offer the action,
    // so the user is never asked to prove and then told their wallet cannot sign.
    if (!publicKey || !signMessage) {
      throw new Error("Connect a wallet that supports message signing.");
    }

    try {
      const result = await transfer(notes, recipientPubkey, amount, {
        connection,
        programId: CLOAK_PROGRAM_ID,
        relayUrl: CLOAK_PRODUCTION_RELAY_URL,

        // ── the two options that authenticate the request ──
        walletPublicKey: addressFromPublicKey(publicKey), // the request's `sender`
        signMessage,               // signs the request preimage
        // ──────────────────────────────────────────────────

        onProgress: (status) => console.log(status),
        onProofProgress: (percent) => console.log(`proof ${percent}%`),
      });

      // Bank the outputs BEFORE you return, and before any UI says "sent". The
      // sender's change note is in here and these bytes are its only copy.
      // Zero-amount outputs are circuit padding, so drop them.
      await persistNotes(
        result.outputUtxos.filter((utxo) => utxo.amount > 0n).map(serializeUtxo),
      );

      return result;
    } catch (error) {
      const hint = explainRelayAuthRejection(String(error));
      throw hint ? new Error(hint, { cause: error }) : error;
    }
  };
}
```

`signMessage` must return the 64-byte ed25519 **detached** signature, which is exactly what
`@solana/wallet-adapter-react` returns. Nothing else about the flow changes between a wallet and a
keypair; only the holder of the pen changes.

<Note>
  `walletPublicKey` and `depositorPublicKey` are two names for the same end user. Pass whichever you
  already have. Passing **both** with different keys throws: the SDK uses that one key for the
  request's sender, for screening, and for viewing-key registration, so splitting it across two keys
  would authenticate one person and screen another.
</Note>

### Server: a local keypair

```typescript theme={null}
import { CLOAK_PROGRAM_ID, CLOAK_PRODUCTION_RELAY_URL, fullWithdraw } from "@cloak.dev/sdk";

// `notes` is one or two notes here, which is the shape that returns nothing to store.
// Three or more makes this call merge on chain first; merge those yourself instead.
await fullWithdraw(notes, recipient, {
  connection,
  programId: CLOAK_PROGRAM_ID,
  relayUrl: CLOAK_PRODUCTION_RELAY_URL,
  depositorKeypair: signer,
  walletPublicKey: signer.address,
});
```

A keypair signs in microseconds and never prompts.

### Neither: the call throws before it proves anything

If a relay-submitted flow has no usable signer, the SDK refuses at the top of the call:

```
Cloak private send requires an authenticated sender, and no signer was provided. Pass either
`depositorKeypair` (a local Keypair) or `signMessage` together with `walletPublicKey` (a browser
wallet adapter). The sender must be the end user's own wallet. Checked before proof generation, so
nothing has been computed or submitted yet.
```

That check is deliberate and it is placed ahead of every `await` in the flow. The alternative, which
is what earlier builds did, was to fetch a Merkle proof, fetch a risk quote, compute a full Groth16
proof, ship it, and hand back an opaque `Unauthorized`: slow **and** uninformative. A half-configured
signer gets its own message naming the missing half.

## The sender must be the end user's own wallet

Whatever key you pass becomes the request's `sender`, and on a shield-to-shield send that is the key
screened for sanctions.

<Warning>
  Never substitute an ephemeral, session or service-held key here. It is not a shortcut around a
  wallet prompt, it moves the screening onto somebody who is not the user. If your architecture makes
  the user's wallet unavailable at send time, the send does not belong on that code path.
</Warning>

The same key is also the one a viewing key is registered against. A keypair paired with a *different*
`walletPublicKey` registers for one wallet and authenticates as the other, and the only symptom is a
rejection saying the authenticated sender has no registered viewing key, about a key you believe is
registered. The SDK catches that pairing up front rather than letting it reach the network.

## The approval window

The timestamp is signed, so it cannot be refreshed after the fact. Two clock facts follow, and both
are exported so you can build UI around them:

| Export                                 | Value | What it means for you                                                    |
| -------------------------------------- | ----- | ------------------------------------------------------------------------ |
| `REQUEST_AUTH_MAX_AGE_SECONDS`         | `300` | a request is valid for 300 seconds from the moment it is signed          |
| `REQUEST_AUTH_MAX_FUTURE_SKEW_SECONDS` | `30`  | a machine clock more than 30 seconds **fast** cannot authenticate at all |

The SDK holds 15 of those 300 seconds back for serializing the body and getting the request onto the
network, so **the real budget a user has to approve a prompt is 285 seconds**. Past that the SDK
fails the call locally, with a message saying nothing was submitted, rather than shipping a proof
that is already dead on arrival.

Two consequences worth designing for:

* **An approval dialog left sitting fails.** A hardware wallet prompt that waits five minutes on a
  desk comes back to a request that has expired. Nothing moved and no funds are at risk, but the
  user has to start the operation again, including the proof. Keep the prompt in front of them.
* **A wrong machine clock fails everything.** In a browser the timestamp comes from the user's own
  laptop. If it runs fast, every request is refused before anything else is even looked at, and the
  fix is on their machine, not in your code.

`explainRelayAuthRejection(responseText)` turns those into something a person can act on. It returns
`null` for anything that is not an authentication rejection, so it is safe to append unconditionally:

```typescript theme={null}
import { explainRelayAuthRejection, fullWithdraw } from "@cloak.dev/sdk";

try {
  // fullWithdraw over one or two notes is used here because that shape is the one
  // that returns no change note to bank. Every other flow on this page, and
  // fullWithdraw over three or more notes, produces secrets you must persist first.
  await fullWithdraw(notes, recipient, options);
} catch (error) {
  const hint = explainRelayAuthRejection(String(error));
  // hint, when non-null, is user-facing prose: "This computer's clock is more than
  // 30 seconds ahead...", "The request was signed more than 300 seconds before it
  // arrived...", "The request body changed after it was signed..."
  showError(hint ?? "Something went wrong. Please try again.");
}
```

## How many wallet prompts to expect

<Note>
  **A private send, or a withdrawal of one or two notes, signs exactly once**, whatever happens on the
  network. Stale-root retries re-use the same proof and the same signed request, so the user sees one
  dialog and no more.
</Note>

A withdrawal of **three or more** notes is the quiet exception. It merges the two smallest notes on
chain until two remain, each merge is its own submitted request, and each request needs its own
approval: budget `n - 1` dialogs for `n` input notes. `maxWalletApprovals` does not cap this, it
applies to swaps only. Read
[a withdrawal of three or more notes is not exempt](/sdk/wallet-integration#a-withdrawal-of-three-or-more-notes-is-not-exempt)
before you offer that shape, because a prompt declined mid-merge is exactly where value is lost.

Swaps are the other exception, because a swap re-proves on every retry and each new proof is a new request
that needs a fresh approval. `maxWalletApprovals` caps how many times one swap may ask:

```typescript theme={null}
import { serializeUtxo, swapWithChange } from "@cloak.dev/sdk";

const swap = await swapWithChange(inputUtxos, amount, outputMint, recipientAta, minOutput, {
  connection,
  programId,
  relayUrl,
  walletPublicKey: addressFromPublicKey(publicKey),
  signMessage,
  chainNoteViewingKeyNk: nk,  // the user's own nk; see the refund note below
  maxWalletApprovals: 3,      // default 5, floored at 1
});

// A swap returns TWO things you must keep, and this call is the last moment
// either exists. Store both before you report success.
await persistNotes(
  swap.outputUtxos.filter((utxo) => utxo.amount > 0n).map(serializeUtxo), // the change note
);
await persistSwapRefund(swap.swapStatePda, swap.refund); // reclaims a timed-out swap
```

* Applies **only** to the wallet-adapter path and **only** to `swapUtxo` / `swapWithChange`.
* Default `5`. Without the cap, `maxRootRetries` alone would allow 41 dialogs for a single swap.
* A `depositorKeypair` never prompts and stays bounded by `maxRootRetries` as before.

<Warning>
  **A swap has a second secret, and it is not a note.** Alongside the change note on `outputUtxos`,
  `swapUtxo` and `swapWithChange` return `refund`: the key material that reclaims the swap principal
  if the swap times out before it executes. Persist it beside the change note, keyed by
  `swap.swapStatePda`. Persisting it is the **fast** path always, and the **only** path when no
  `chainNoteViewingKeyNk` was passed to the swap, because that is the case where `refund` is the only
  copy that will ever exist. When an `nk` **was** passed, `refund.derivedFromNk` comes back `true` and
  `discoverSwapRefunds` / `matchSwapRefundLeaf` can rebuild the same secret from the `nk` plus the
  swap's public input nullifier, so losing the object is recoverable rather than terminal.
</Warning>

## Driving submission yourself

Most integrations never need this. Reach for it when you are running your own retry or queueing
layer around submission.

`submitTransactToRelay` is the supported seam. It takes the same two alternatives, a
`depositorKeypair` or a `relayAuthSigner`, and handles the signing, the wire fields and the on-chain
settlement check for you:

```typescript theme={null}
import {
  addressFromPublicKey,
  messageSignerFromCallback,
  submitTransactToRelay,
  type RelayAuthSigner,
} from "@cloak.dev/sdk";

// Since 0.2.5 `RelayAuthSigner` is a Kit signer (`MessageSigner | TransactionSigner`),
// not a `{ walletPublicKey, signMessage }` pair. Build one from the adapter:
const relayAuthSigner: RelayAuthSigner = messageSignerFromCallback(
  addressFromPublicKey(publicKey), // the end user's real wallet, never an ephemeral key
  signMessage,
);

const result = await submitTransactToRelay({
  relayUrl,
  requestBody, // the exact body you will POST
  programId,
  relayAuthSigner,
  settlement: { connection, programId, mint, inputNullifiers },
  canRetryStaleRoot: false,
});
// result.kind: "submitted" | "stale-root" | "failed"
```

### Signing a body by hand

Below that, `buildRelayAuthPreimage` gives you the bytes to sign without signing them, which is the
only shape a wallet adapter can work with:

```typescript theme={null}
import { TRANSACT_AUTH_FIELDS, buildRelayAuthPreimage } from "@cloak.dev/sdk";

// `auth_signature` is base64. Do this without `Buffer`: it is a Node global, so a
// browser bundle fails to compile against it and throws `Buffer is not defined` at
// runtime unless you have polyfilled it. `btoa` is present in browsers and in Node.
const toBase64 = (bytes: Uint8Array) =>
  btoa(String.fromCharCode(...Array.from(bytes)));

const preimage = buildRelayAuthPreimage(
  "/transact",          // the exact path you POST to
  programId,
  requestBody,
  publicKey,            // the end user's wallet, becomes `sender`
  undefined,            // issued-at seconds; omit to stamp now
  TRANSACT_AUTH_FIELDS, // use TRANSACT_SWAP_AUTH_FIELDS for a swap body
);

const signature = await signMessage(preimage.message);

await fetch(`${relayUrl}/transact`, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({
    ...requestBody,
    sender: preimage.sender,
    auth_issued_at: preimage.auth_issued_at,
    auth_nonce: preimage.auth_nonce,
    auth_signature: toBase64(signature),
  }),
});
```

`preimage.message` is a plain ed25519 detached-signature preimage, newline separated:

```
CLOAK_RELAY_REQUEST_AUTH_V1
<endpoint>
<program id, base58>
<auth_nonce>
<auth_issued_at>
<sha256 hex of canonicalJson(signed view)>
```

The **signed view** is your body reduced to exactly the fields in the list you passed, plus `sender`,
with every field you omitted written as an explicit `null`. `canonicalJson` is exported so you can
reproduce that digest yourself: keys sorted bytewise, no whitespace. It refuses values that the two
sides could render differently (non-integer or beyond-safe-integer numbers, functions, symbols)
rather than signing a digest that cannot match, so keep `u64` amounts as decimal strings.

`buildRelayAuthPreimage` generates the nonce and timestamp once per call. **If you re-POST a
request, re-send the same preimage and the same body bytes.** Rebuilding it produces a brand-new
request, not a retry.

<Warning>
  **`slippage_bps` must always be present in a signed swap body.** It is the one field in
  `TRANSACT_SWAP_AUTH_FIELDS` that cannot be represented as `null` on the other side: omit it and you
  sign `"slippage_bps":null` while the receiving side signs its own default of `500`. The digests
  diverge, and the rejection never mentions slippage. Every other field in both lists signs as an
  explicit `null` when absent, which is why this one is the trap. The SDK's own swap path always sets
  it; hand-built bodies must too, and `buildRelayAuthPreimage` will refuse the call rather than sign a
  digest that cannot match.
</Warning>

The 9 transact fields and the 19 swap fields are exported as `TRANSACT_AUTH_FIELDS` and
`TRANSACT_SWAP_AUTH_FIELDS`. Read them from the package rather than retyping them; the order is
part of the agreement.

## Which endpoint the SDK talks to, and which RPC you use

These are two separate things and they are easy to conflate.

**The submission endpoint is pinned when the SDK is built.** `relayUrl` no longer selects it. The
published build accepts exactly two values:

* an origin on this build's allowlist, best written as the exported `CLOAK_PRODUCTION_RELAY_URL`
  rather than typed by hand. Anything else throws, naming the value and the allowlist.
* `""`, the caller-signed direct-submission signal, meaning no endpoint at all: you sign and submit
  yourself. On its own it still throws; see the warning below.

There is no default. Omitting `relayUrl` yields `undefined`, never production. `riskQuoteUrl` is held
to the same allowlist, because it is converted back into a base URL, so it must be an absolute URL on
the allowlist rather than a relative path.

<Warning>
  **`""` is not a working value by itself.** Omitting `relayUrl`, or passing `""`, makes every flow
  **including deposits** throw `Viewing key registration is mandatory: relayUrl is required.` before
  any network call. The direct-submission half only takes effect once
  `enforceViewingKeyRegistration` is **also** set to `false`, because the viewing-key gate defaults on
  and fires first. That pair is not something to reach for: with a signer present it submits a
  transfer or withdrawal under the user's own key, publicly linking it.
</Warning>

<Note>
  **A mistyped `relayUrl` is not a `401`, and the two failures live in different places.** A value
  off the allowlist throws **locally**, naming the value you passed and the allowlist, before anything
  is signed and before any request leaves the process. Nothing was submitted and nothing was refused,
  so authentication is not what went wrong; import `CLOAK_PRODUCTION_RELAY_URL` instead of typing the
  string. A `401`, an `Unauthorized`, or a rejection that `explainRelayAuthRejection` can explain means
  the opposite: the endpoint was accepted, the request arrived, and the **signed sender** is what was
  turned away. Only that second case belongs on this page.
</Note>

<Note>
  **Your RPC is entirely your own choice.** The pin covers the submission endpoint only. You build the
  `CloakRpc` with `createCloakRpc(rpcUrl)` and Cloak never inspects, replaces or proxies it. Any public RPC, any private provider,
  any paid endpoint works. The one exception is a **loopback** RPC (`localhost`, `127.0.0.1`), which a
  production build refuses because a production artifact talking to a local validator is always a
  mistake. "You have to use Cloak's RPC" is a misreading of this. You do not.
</Note>

### One more origin: the circuits bundle

**A proof is generated on the end user's own device, so the proving artifacts have to reach it.**
On the first proof of a page's life the SDK fetches two files from the bundle base this build was
compiled against, exported as `DEFAULT_TRANSACTION_CIRCUITS_URL`:

```
https://storage.googleapis.com/cloak-circuits/circuits/0.2.0/transaction_js/transaction.wasm    3.2 MB
https://storage.googleapis.com/cloak-circuits/circuits/0.2.0/transaction_final.zkey            19.7 MB
```

Just under 23 MB together, and those sizes are exact rather than approximate: both files are
hash-pinned in the SDK build and re-checked against the pinned digests before a byte of either
reaches the prover, so bytes of any other size do not prove.

**This one reaches deposits too.** Request authentication is the mechanism a deposit skips; proof
generation is not. Every row of the table at the top of this page proves, so a policy that blocks the
bundle breaks the [Quickstart](/sdk/quickstart) deposit as well, on the very first call you make.

<Warning>
  **A strict Content-Security-Policy has to name this origin.** The artifacts arrive over an ordinary
  `fetch`, so `connect-src` is the directive that governs them, and the witness generator is then
  compiled as WebAssembly inside the page, which a strict `script-src` has to permit separately:

  ```
  connect-src 'self' https://storage.googleapis.com ...;
  script-src  'self' 'wasm-unsafe-eval' ...;
  ```

  Miss the `connect-src` entry and the browser refuses the request before it leaves the page. The
  failure then surfaces from inside proof generation as a failed fetch naming the artifact URL, which
  points at the SDK rather than at the policy that actually blocked it. The line that names your
  policy is in the browser console, not in the error you catch.
</Warning>

**Caching is in memory and per page load.** Verified artifact bytes are memoised for the life of the
process and keyed by base, so a user who sends three times downloads once, not three times. The SDK
keeps no persistent cache of its own: a reload starts from an empty memo, and whether anything is
actually re-downloaded is left to the browser's ordinary HTTP cache. If you would rather pay that
cost on mount than at the first click, warm it yourself; on success the check memoises the same bytes
the prove path later reads.

```typescript theme={null}
import { getCircuitsPath, verifyAllCircuits } from "@cloak.dev/sdk";

// Fire and forget on mount. The first proof then finds the bytes already loaded.
void verifyAllCircuits(getCircuitsPath());
```

<Note>
  **This is one asset origin, not a claim on your infrastructure.** The request carries no body, no
  headers of yours and none of your user's data; it is a static download of the public ceremony bundle,
  and the digest pin is why fetching it from a Cloak-controlled host is safe rather than trusting.
  It differs from the RPC in one way worth knowing before you plan around it: like the submission
  endpoint, the base is fixed when the SDK is built, so re-serving the artifacts from your own CDN is
  not a call option. `setCircuitsPath()` does accept a local directory
  holding the same two files, which covers an offline or air-gapped Node process, but a browser has no
  filesystem to read and refuses that shape.
</Note>

### Pointing at a non-production endpoint

You cannot. The allowed origins are decided when the SDK is built, so a published build reaches
exactly one submission endpoint and nothing a consumer supplies changes that — not a call option,
not an environment variable, not a bundler define. `CLOAK_PRODUCTION_RELAY_URL` is the value to
pass; anything else throws before a request is made, naming what you passed and what this build is
pinned to. The loopback-RPC guard (`BUILD_ALLOWS_LOCAL_ENDPOINTS`) is derived from the same pin, so
a production build also refuses a `localhost` RPC.

If you need a non-production target for an integration rehearsal, talk to us rather than working
around the pin.

<Note>
  **Why this is not an environment variable.** An env var, a `NODE_ENV` check or a bundler define all
  resolve inside the consumer's process at the consumer's build or run time. That is precisely the
  moment the pin exists to constrain, so a value read there pins nothing. The only value a published
  artifact carries that a consumer cannot supply is one decided when the artifact was built. It is an
  integrity and product-control mechanism, not a security boundary: anyone who can run code in the
  consumer's process can patch it. What it buys is that the correct endpoint is the only one reachable
  **by accident**.
</Note>

## Related pages

<CardGroup cols={2}>
  <Card title="Wallet integration" icon="wallet" href="/sdk/wallet-integration">
    Full wallet-adapter wiring, progress callbacks and stale-root retries.
  </Card>

  <Card title="Error handling" icon="triangle-exclamation" href="/sdk/error-handling">
    `CloakError` categories, `parseError`, and the stale-root retry template.
  </Card>

  <Card title="Shielded transfers" icon="shuffle" href="/sdk/shielded-transfers">
    What a private send actually does, and how the recipient gets the note.
  </Card>

  <Card title="API reference" icon="code" href="/sdk/api-reference">
    The full exported surface, including every symbol on this page.
  </Card>
</CardGroup>
