> For the complete documentation index, see [llms.txt](https://docs.zama.org/protocol/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.zama.org/protocol/confidential-vault/getting-started.md).

# Getting Started

Make your first confidential vault deposit on Sepolia, from an empty folder to a decrypted position.

In this tutorial we will write a small script that deposits mock USDC into a confidential vault on Sepolia, then decrypt the resulting position with our own key. Along the way we use every moving part of the protocol once: the wrapper, the batcher, an encrypted input, and a user decryption.

Everything runs on the Sepolia staging deployment. Its USDC is a mock with a public `mint`, and its batches are dispatched every 58 minutes instead of once a day as on mainnet, so the whole tutorial completes within about an hour.

## Before we start

You need:

* **Node.js 22+** (or Bun).
* **A Sepolia account with a little ETH** for gas — about 0.005 ETH covers the whole tutorial. Any public faucet works.
* Its private key, exported as an environment variable.

{% stepper %}
{% step %}

### Create the project

```bash
mkdir confidential-vault-tutorial && cd confidential-vault-tutorial
npm init -y && npm pkg set type=module
npm install @zama-fhe/sdk viem tsx
```

Store your key in the environment — never in a file:

```bash
export PRIVATE_KEY=0x...   # your funded Sepolia key
```

{% endstep %}

{% step %}

### Set up the clients and the SDK

Create `deposit.ts` with the wiring: viem clients for the chain, and the Zama SDK for everything encrypted. The `sepolia` preset from `@zama-fhe/sdk/chains` carries every FHE system-contract address, so this is all the FHE configuration there is:

{% code title="deposit.ts" %}

```ts
import { ZamaSDK, memoryStorage } from "@zama-fhe/sdk";
import { sepolia as zamaSepolia } from "@zama-fhe/sdk/chains";
import { createConfig } from "@zama-fhe/sdk/viem";
import { node } from "@zama-fhe/sdk/node";
import {
  createPublicClient,
  createWalletClient,
  http,
  parseAbi,
  erc20Abi,
  decodeEventLog,
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { sepolia } from "viem/chains";

const RPC_URL = "https://ethereum-sepolia-rpc.publicnode.com";
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);

const publicClient = createPublicClient({ chain: sepolia, transport: http(RPC_URL) });
const walletClient = createWalletClient({ account, chain: sepolia, transport: http(RPC_URL) });

const chain = { ...zamaSepolia, network: RPC_URL };
const sdk = new ZamaSDK(
  createConfig({
    chains: [chain],
    relayers: { [chain.id]: node() },
    publicClient,
    walletClient,
    storage: memoryStorage,
  }),
);

// Sepolia staging deployment, vault `steakhouse-usdc-high-yield` — see the Contract Addresses reference.
const USDC = "0x9b5Cd13b8eFbB58Dc25A05CF411D8056058aDFfF";
const CUSDC = "0x7c5BF43B851c1dff1a4feE8dB225b87f2C223639";
const DEPOSIT_BATCHER = "0x5964855395836727Cf62E32d534B54Ccd855eB03";
const CSHARE = "0x9A4Ae4951A756B4742ba3a330911af6c1502eA70";

const WRAPPER_ABI = parseAbi([
  "function wrap(address to, uint256 amount) returns (bytes32)",
  "function confidentialBalanceOf(address account) view returns (bytes32)",
  "function confidentialTransferAndCall(address to, bytes32 amount, bytes inputProof, bytes data) returns (bytes32)",
]);

const BATCHER_ABI = parseAbi([
  "function batchState(uint256 batchId) view returns (uint8)",
  "function claim(uint256 batchId, address account) returns (bytes32)",
  "event Joined(uint256 indexed batchId, address indexed account, bytes32 amount)",
]);
```

{% endcode %}
{% endstep %}

{% step %}

### Mint mock USDC

The staging USDC has a public `mint`. We will deposit 25 USDC (6 decimals). Append to `deposit.ts`:

{% code title="deposit.ts (continued)" %}

```ts
const AMOUNT = 25_000_000n; // 25 USDC

const MOCK_ABI = parseAbi(["function mint(address to, uint256 amount)"]);
const mintHash = await walletClient.writeContract({
  address: USDC,
  abi: MOCK_ABI,
  functionName: "mint",
  args: [account.address, AMOUNT],
});
await publicClient.waitForTransactionReceipt({ hash: mintHash });
console.log("minted 25 mock USDC");
```

{% endcode %}

Run what we have so far:

```bash
npx tsx deposit.ts
```

You should see `minted 25 mock USDC`. If you see a gas error instead, the account has no Sepolia ETH yet.
{% endstep %}

{% step %}

### Shield: wrap USDC into confidential cUSDC

Approve the wrapper, then wrap. `wrap` takes a cleartext amount and mints the encrypted balance on-chain, so this step is public — the confidentiality starts inside the batch:

{% code title="deposit.ts (continued)" %}

```ts
const approveHash = await walletClient.writeContract({
  address: USDC,
  abi: erc20Abi,
  functionName: "approve",
  args: [CUSDC, AMOUNT],
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });

const wrapHash = await walletClient.writeContract({
  address: CUSDC,
  abi: WRAPPER_ABI,
  functionName: "wrap",
  args: [account.address, AMOUNT],
});
await publicClient.waitForTransactionReceipt({ hash: wrapHash });
console.log("shielded: 25 USDC → 25 cUSDC");
```

{% endcode %}

Our public USDC balance is now zero, and `confidentialBalanceOf` on cUSDC returns a 32-byte handle pointing at a ciphertext. That handle is what the rest of the protocol computes on.
{% endstep %}

{% step %}

### Join the deposit batch

Now the step that makes the deposit confidential. We encrypt the amount locally — the SDK produces a ciphertext handle plus a proof binding it to the cUSDC contract and to our address — and send it to the batcher with a single confidential transfer:

{% code title="deposit.ts (continued)" %}

```ts
const encrypted = await sdk.encrypt({
  values: [{ type: "euint64", value: AMOUNT }],
  contractAddress: CUSDC,
  userAddress: account.address,
});

const joinHash = await walletClient.writeContract({
  address: CUSDC,
  abi: WRAPPER_ABI,
  functionName: "confidentialTransferAndCall",
  args: [DEPOSIT_BATCHER, encrypted.encryptedValues[0]!, encrypted.inputProof, "0x"],
});
const receipt = await publicClient.waitForTransactionReceipt({ hash: joinHash });

let batchId!: bigint;
for (const log of receipt.logs) {
  if (log.address.toLowerCase() !== DEPOSIT_BATCHER.toLowerCase()) continue;
  try {
    const decoded = decodeEventLog({ abi: BATCHER_ABI, data: log.data, topics: log.topics });
    if (decoded.eventName === "Joined") batchId = decoded.args.batchId;
  } catch {}
}
console.log(`joined deposit batch ${batchId}`);
```

{% endcode %}

Run the script again. It mints, shields, joins — and prints the batch id it landed in. Open the join transaction on [Sepolia Etherscan](https://sepolia.etherscan.io): the `Joined` event's amount field is a handle, and nowhere in the transaction does `25` appear.
{% endstep %}

{% step %}

### Watch the batch settle

The batch now moves through its lifecycle: dispatched (the aggregate is sent for decryption), then finalized (the pooled USDC enters the vault and the exchange rate freezes). On this staging deployment an operator dispatches each batch once it is 58 minutes old, so we can poll and wait:

{% code title="deposit.ts (continued)" %}

```ts
const STATES = ["Pending", "Dispatched", "Finalized", "Canceled"] as const;

let state = 0;
while (state < 2) {
  state = await publicClient.readContract({
    address: DEPOSIT_BATCHER,
    abi: BATCHER_ABI,
    functionName: "batchState",
    args: [batchId],
  });
  console.log(`batch ${batchId}: ${STATES[state]}`);
  if (state < 2) await new Promise((r) => setTimeout(r, 15_000));
}
```

{% endcode %}

You will see `Pending`, then `Dispatched`, then — once the Zama Protocol returns the decrypted aggregate — `Finalized`. Expect up to an hour in `Pending` and a few minutes after that. Leave the script running, or stop it and rerun only the steps below with the batch id it printed.
{% endstep %}

{% step %}

### Claim the confidential shares

{% code title="deposit.ts (continued)" %}

```ts
try {
  const claimHash = await walletClient.writeContract({
    address: DEPOSIT_BATCHER,
    abi: BATCHER_ABI,
    functionName: "claim",
    args: [batchId, account.address],
  });
  await publicClient.waitForTransactionReceipt({ hash: claimHash });
  console.log("claimed confidential vault shares");
} catch {
  console.log("already claimed — the operator delivered it for us");
}
```

{% endcode %}

Claims are open for anyone to deliver, and the staging operator airdrops them — so your claim may have already arrived while you polled. Either way, the shares now sit in your confidential cShare balance.
{% endstep %}

{% step %}

### Decrypt your position

Read the cShare balance handle and decrypt it. Only our key can do this — the same request from any other account fails at the access-control layer:

{% code title="deposit.ts (continued)" %}

```ts
const shareHandle = await publicClient.readContract({
  address: CSHARE,
  abi: WRAPPER_ABI,
  functionName: "confidentialBalanceOf",
  args: [account.address],
});

const clear = await sdk.decryption.decryptValues([
  { encryptedValue: shareHandle, contractAddress: CSHARE },
]);
console.log(`confidential vault shares: ${clear[shareHandle]}`);
```

{% endcode %}

The printed number is your share of the batch at its frozen exchange rate — visible to you, ciphertext to everyone else.
{% endstep %}
{% endstepper %}

{% hint style="success" %}
**You made a confidential deposit** You shielded a public token, joined a batch with an encrypted amount, watched the batch settle against a real ERC-4626 vault, and decrypted a position only you can see. Every mainnet integration is this same sequence with different addresses and slower batches.
{% endhint %}

## Where to go next

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Deposit into a Vault</strong></td><td>The production-grade version of this flow, with every pitfall covered.</td><td><a href="/protocol/confidential-vault/guides/deposit.md">Deposit into a Vault</a></td></tr><tr><td><strong>Batch Lifecycle</strong></td><td>What Pending, Dispatched, Finalized, and Canceled really mean.</td><td><a href="/protocol/confidential-vault/concepts/batch-lifecycle.md">Batch Lifecycle</a></td></tr><tr><td><strong>Withdraw from a Vault</strong></td><td>The two exits: confidential and batched, or instant and public.</td><td><a href="/protocol/confidential-vault/guides/withdraw.md">Withdraw from a Vault</a></td></tr><tr><td><strong>Confidentiality</strong></td><td>Exactly what an observer can and cannot see.</td><td><a href="/protocol/confidential-vault/concepts/confidentiality.md">Confidentiality</a></td></tr></tbody></table>


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.zama.org/protocol/confidential-vault/getting-started.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
