> For AI agents: the documentation index is at https://sdk.nimbusprivacy.xyz/llms.txt, and every page is available as Markdown at its URL with .md appended.

# Quickstart (/getting-started/quickstart)

## What you'll build

Alice deposits 25 USDC into her private balance, then pays Bob 10 USDC from it. Bob takes his
USDC back out to his own wallet.

Everything runs on mainnet, so it moves real USDC. Each step below is a few lines of browser code.

## Install

```bash
pnpm add @umbra/nimbus-solana-web @solana/kit
```

You use `@solana/kit` for its `address` function, which turns a base58 string into an `Address`.

## Serve the SDK's files

The SDK ships WebAssembly and a worker script that your app serves itself. Copy them into your
static folder after every install, and keep that folder out of git:

```json title="package.json"
{
  "scripts": {
    "postinstall": "nimbus-solana-web copy-assets public/nimbus"
  }
}
```

The page that loads the SDK also needs two headers. They let the proofs use shared memory.

**Next.js**

```ts
const isolation = [
  { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
  { key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
];
export default { headers: async () => [{ source: "/:path*", headers: isolation }] };
```

**Vite**

```ts
const headers = {
  "Cross-Origin-Opener-Policy": "same-origin",
  "Cross-Origin-Embedder-Policy": "require-corp",
};
export default { server: { headers }, preview: { headers } };
```

[Install and set up](https://sdk.nimbusprivacy.xyz/getting-started/setup) explains both, and covers Vercel and Cloudflare.

## Load the SDK

Load it once per page, in code that runs in the browser. It needs no wallet yet.

```ts
import { loadNimbusSdk } from "@umbra/nimbus-solana-web";

const sdk = await loadNimbusSdk(
  { cluster: "mainnet" },
  { assetsBaseUrl: "/nimbus/" },
);
```

`assetsBaseUrl` is the URL of the folder you copied the files into. In React, load it from an
effect; [Use it with React](https://sdk.nimbusprivacy.xyz/getting-started/react) shows how.

## Connect a wallet

`listWallets` returns the installed wallets that can sign on mainnet. Show them, and connect the
one Alice picks.

```ts
import { listWallets, openWalletConnection } from "@umbra/nimbus-solana-web";

const [wallet] = listWallets(sdk);
if (wallet === undefined) throw new Error("Install a Solana wallet first.");

const connection = await openWalletConnection(sdk, { wallet });
switch (connection.kind) {
  case "declined":
    throw new Error("Alice declined to connect.");
  case "no-account":
    throw new Error("The wallet has no account on Solana mainnet.");
  case "connected":
    break;
}
const { signer, transactionVersion } = connection.connected;
```

Keep `transactionVersion`. It says which transactions the wallet can sign, and every payment her
wallet signs takes it.

## Open Alice's private account

Opening a session asks Alice's wallet to sign one message. That signature unlocks her private
account on this device. No money moves, and it costs nothing.

```ts
import { openNimbusSession } from "@umbra/nimbus-solana-web";

const opened = await openNimbusSession(
  sdk,
  { connectedWallet: signer },
  { gatewayToken },
);
if (opened.kind === "declined") throw new Error("Alice declined to sign.");
const { session } = opened;
```

The gateway token lets your app use Nimbus's services on mainnet. To get one, email [dev@umbraprivacy.com](mailto:dev@umbraprivacy.com) or reach out to any Umbra
team member.
[The mainnet gateway token](https://sdk.nimbusprivacy.xyz/getting-started/setup#the-mainnet-gateway-token) shows how to pass it.

## Register her

Each wallet registers once, before its first deposit. `session.registration` says whether Alice
already has.

```ts
import { createRegistrationContext, register } from "@umbra/nimbus-solana-web";

if (session.registration.kind === "unregistered") {
  const result = await register(
    await createRegistrationContext(session),
    {},
    { transactionVersion },
  );
  switch (result.kind) {
    case "complete":
      break;
    case "submitted":
      if (result.submission.submission.kind !== "landed") {
        throw new Error("The registration did not land. Try again.");
      }
      break;
    case "declined":
    case "store-refused":
      throw new Error("Nothing was sent. Try again.");
  }
}
```

Her wallet approves one transaction and pays its network fee. [Registration](https://sdk.nimbusprivacy.xyz/registration)
covers every result.

## Deposit 25 USDC

USDC has 6 decimals, so 25 USDC is `25_000_000n`. Alice deposits into her own private balance.

```ts
import { address } from "@solana/kit";
import { createDepositContext, deposit } from "@umbra/nimbus-solana-web";

const usdc = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");

const result = await deposit(
  await createDepositContext(session),
  { mint: usdc, amount: 25_000_000n, recipient: session.walletAddress },
  { transactionVersion },
);
if (
  result.kind !== "submitted" ||
  result.submission.submission.kind !== "landed"
) {
  throw new Error("The deposit did not land.");
}
```

The protocol fee comes out of the 25 USDC. [Deposit](https://sdk.nimbusprivacy.xyz/deposit) handles every result, and the
`unknown` case where Solana has not decided yet.

## Read her balance

The private balance lives on Alice's device. Sync it with Solana first, then read it.

```ts
import type { Balance } from "@umbra/nimbus-solana-web";
import { readBalances, syncNoteStore } from "@umbra/nimbus-solana-web";

await syncNoteStore(session, {});
const balances = await readBalances(session, {});

const usdcIn = (state: Balance["state"]): bigint =>
  balances.find((row) => row.mint === usdc && row.state === state)?.total ??
  0n;

console.log("Available:", usdcIn("available"));
console.log("Reserved:", usdcIn("reserved"));
```

Each row is one token in one state. A new deposit reads as `pending` until Solana finalizes it,
then as `available`. `reserved` is what a payment still in progress holds.
[Balances](https://sdk.nimbusprivacy.xyz/helpers/balances) explains every state.

## Pay Bob 10 USDC privately

A Private Send moves value from Alice's private balance to Bob's. Bob must have registered too. If
he has not, `transfer` throws a `NimbusSdkError` with reason `recipient-unregistered`.

```ts
import { createTransferContext, transfer } from "@umbra/nimbus-solana-web";
import type { TransferResult } from "@umbra/nimbus-solana-web";

const result = await transfer(await createTransferContext(session), {
  mint: usdc,
  amount: 10_000_000n,
  recipient: bob,
});

function describeTransfer(result: TransferResult): string {
  switch (result.kind) {
    case "relayed":
      switch (result.settlement.kind) {
        case "landed":
          return "Bob has been paid.";
        case "failed":
          return "The payment failed on Solana. Nothing moved.";
        case "pending":
        case "not-followed":
          return "The payment is on its way.";
      }
    case "refused":
      return `Nothing was sent: ${result.refusal.kind}.`;
    case "relay-refused":
    case "unsent":
    case "merge-failed":
      return "The payment was not sent. Try again.";
  }
}
console.log(describeTransfer(result));
```

A relayer submits the payment, so Alice's wallet signs nothing and pays no network fee. The call
then follows it until it settles on Solana. [Private Send](https://sdk.nimbusprivacy.xyz/transfer) covers each result.

## Bob withdraws

On his own device, Bob has opened his session and registered, as Alice did. He withdraws 5 USDC
to his wallet.

```ts
import { createWithdrawalContext, withdraw } from "@umbra/nimbus-solana-web";

const result = await withdraw(await createWithdrawalContext(bobSession), {
  mint: usdc,
  amount: 5_000_000n,
  recipient: bobSession.walletAddress,
});
if (result.kind === "relayed" && result.settlement.kind === "landed") {
  console.log("Bob withdrew 5 USDC.");
}
```

The withdrawal syncs Bob's device first, so it sees Alice's payment. A relayer submits it too.
[Withdraw](https://sdk.nimbusprivacy.xyz/withdraw) covers every result.

> **The objects you hold**
>
> You load one **SDK** per page. You open one **session** per user, when they connect. You create
> one **context** per payment, from the session. Every function takes one of these first.
> [The function template](https://sdk.nimbusprivacy.xyz/api-design/function-template) explains the pattern.

## The whole file

**The whole file**

```ts
import { address } from "@solana/kit";
import {
  createDepositContext,
  createRegistrationContext,
  createTransferContext,
  createWithdrawalContext,
  deposit,
  listWallets,
  loadNimbusSdk,
  openNimbusSession,
  openWalletConnection,
  readBalances,
  register,
  syncNoteStore,
  transfer,
  withdraw,
} from "@umbra/nimbus-solana-web";

const usdc = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");

// Load the SDK.
const sdk = await loadNimbusSdk(
  { cluster: "mainnet" },
  { assetsBaseUrl: "/nimbus/" },
);

// Connect a wallet.
const [wallet] = listWallets(sdk);
if (wallet === undefined) throw new Error("Install a Solana wallet first.");
const connection = await openWalletConnection(sdk, { wallet });
if (connection.kind !== "connected") throw new Error("Not connected.");
const { signer, transactionVersion } = connection.connected;

// Open Alice's private account.
const opened = await openNimbusSession(
  sdk,
  { connectedWallet: signer },
  { gatewayToken },
);
if (opened.kind === "declined") throw new Error("Alice declined to sign.");
const { session } = opened;

// Register her, once.
if (session.registration.kind === "unregistered") {
  const registration = await register(
    await createRegistrationContext(session),
    {},
    { transactionVersion },
  );
  const registered =
    registration.kind === "complete" ||
    (registration.kind === "submitted" &&
      registration.submission.submission.kind === "landed");
  if (!registered) throw new Error("The registration did not land.");
}

// Deposit 25 USDC.
const deposited = await deposit(
  await createDepositContext(session),
  { mint: usdc, amount: 25_000_000n, recipient: session.walletAddress },
  { transactionVersion },
);
if (
  deposited.kind !== "submitted" ||
  deposited.submission.submission.kind !== "landed"
) {
  throw new Error("The deposit did not land.");
}

// Read her balance.
await syncNoteStore(session, {});
const balances = await readBalances(session, {});
for (const row of balances) {
  if (row.mint === usdc) console.log(row.state, row.total);
}

// Pay Bob 10 USDC privately.
const paid = await transfer(await createTransferContext(session), {
  mint: usdc,
  amount: 10_000_000n,
  recipient: bob,
});
if (paid.kind === "relayed" && paid.settlement.kind === "landed") {
  console.log("Bob has been paid.");
}

// Bob withdraws 5 USDC, from his own session.
const withdrawn = await withdraw(await createWithdrawalContext(bobSession), {
  mint: usdc,
  amount: 5_000_000n,
  recipient: bobSession.walletAddress,
});
if (withdrawn.kind === "relayed" && withdrawn.settlement.kind === "landed") {
  console.log("Bob withdrew 5 USDC.");
}
```

## Next

- [Install and set up](https://sdk.nimbusprivacy.xyz/getting-started/setup): Every header, file and token your app needs, in detail.
- [Use it with React](https://sdk.nimbusprivacy.xyz/getting-started/react): Load once, follow wallets, keep the session in context.
- [Private balances](https://sdk.nimbusprivacy.xyz/explainers/private-balance): Where the balance lives, and why the device syncs.
- [Production checklist](https://sdk.nimbusprivacy.xyz/production/checklist): What to check before real users arrive.


Last updated 21 October 2026
