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

# Zama API

> The full Zama confidential vault lifecycle through the Earn API: wrap into a confidential token, encrypt amounts with the Zama SDK, enter, reveal balances, exit privately or instantly, and cancel pending requests.

This guide covers the full lifecycle of a Zama confidential vault through the Earn API: discover the vaults, get the confidential token, encrypt amounts with the Zama SDK, enter, read and reveal balances, exit, and cancel a pending request. The request, sign, submit and poll loop is the same as for any other yield.

What is different is the amount. Your app encrypts every amount on the client with the Zama SDK and sends the API an encrypted handle and a proof. The API never sees a plain amount, and balances come back encrypted until the owner reveals them with a signature.

For how the vaults work, see [Zama](https://docs.yield.xyz/documentation/earn/defi/vaults/zama.md). To embed a ready-made interface instead of building your own, see the [Zama widget](https://docs.yield.xyz/guides/zama/widget.md) guide.

## At a glance

| Item | Value |
| - | - |
| Base URL | `https://api.yield.xyz/v1` |
| Provider | `zama` |
| Networks | Ethereum. Ethereum Sepolia for testing. |
| SDK | `@zama-fhe/sdk` `3.7.0` |
| Relayer | `https://api.yield.xyz/v1/zama/relayer/{chainId}/`, with your Yield.xyz API key as the SDK's `ApiKeyHeader` auth. No Zama key needed. |
| API key | The same project key you use for the rest of the Earn API |

## Prerequisites

* **Zama yields enabled for your project.** Enable them in the Yield.xyz dashboard, from your project's [enabled-yields list](https://docs.yield.xyz/documentation/advanced-setup/dashboard/yield-blocking.md). If you do not have a project yet, [set one up](https://docs.yield.xyz/documentation/get-started/onboarding.md) first. Start with the Sepolia vault.
* **ETH for gas on Ethereum.** A deposit has a high gas limit: at least 1,500,000 gas for each vault leg. The node checks that the wallet can pay the gas limit at the maximum fee before it accepts the transaction, so keep more ETH in the wallet than the deposit usually costs.
* **A signer that signs EIP-712 typed data and sends transactions.** Revealing a balance needs an EIP-712 permit signed by the owner. Every action returns unsigned transactions that the same wallet signs.
* **The Zama SDK.** Install `@zama-fhe/sdk@3.7.0`. The [Zama SDK docs](https://docs.zama.org/protocol/sdk/) cover it in full. The examples use [viem](https://viem.sh).

> [!NOTE]
>
> We construct. You sign. Every action returns unsigned transactions. Yield.xyz never holds keys and never signs on your behalf. Inspect `unsignedTransaction` before you sign it, and never modify it.

## Basics

| Term | Meaning |
| - | - |
| Confidential token | An ERC-7984 token whose balances are encrypted on-chain. Each vault takes a confidential token in (for example cUSDC for USDC) and issues a confidential share token. You get the confidential token by wrapping the plain token. |
| Encrypted handle | A `bytes32` reference to an encrypted value. The SDK returns one handle per encrypted value, plus one input proof for the whole bundle. A handle and its proof are bound to one wallet and one contract, and they only work for that pair. |
| Leg | One encrypted amount per Zama vault of the same asset. Every request sends a leg to each of those vaults: your amount for the chosen vault and an encrypted zero for the others, so the chosen vault stays private. The legs are encrypted for the router contract. There are at most 10. |
| Batch | Deposits and redemptions join a daily batch that settles against the vault. The yield's `investmentSchedule` gives the cutoff times. A position starts earning when its batch settles. |
| Amount units | Encrypted amounts are base units of the confidential token, as a `uint64`. Deposits are in the confidential token. Exits are in the confidential share token. |

The examples use these variables:

```bash theme={null}
export YIELD_API_KEY=<your-api-key>
export ADDR=<your wallet EVM address>
export YIELD_ID=ethereum-usdc-zama-steakhouse-usdc-confidential-vault
```

## The lifecycle

Ten steps cover every flow, in this order: discover, get the confidential token, set up the SDK, encrypt, enter, sign and submit, read and reveal balances, exit privately, exit instantly, and cancel.

### 1. Discover the vaults

```bash cURL theme={null}
curl -s "https://api.yield.xyz/v1/yields?providers=zama" -H "x-api-key: $YIELD_API_KEY"
curl -s "https://api.yield.xyz/v1/yields/$YIELD_ID" -H "x-api-key: $YIELD_API_KEY"
```

```json Response (excerpt) theme={null}
{
  "id": "ethereum-usdc-zama-steakhouse-usdc-confidential-vault",
  "network": "ethereum",
  "token": { "symbol": "USDC", "address": "0x...", "decimals": 6 },
  "tokens": [
    { "symbol": "USDC", "address": "0x..." },
    { "symbol": "cUSDC", "address": "0x..." }
  ],
  "status": { "enter": true, "exit": true },
  "mechanics": {
    "arguments": {
      "enter": {
        "fields": [
          { "name": "inputToken", "required": false, "options": ["..."] },
          { "name": "encryptedAmount", "required": true, "options": ["0x... (confidential token)"] },
          { "name": "encryptedAmountProof", "required": true },
          { "name": "encryptedLegAmounts", "required": true, "options": ["0x... (deposit batcher, leg 0)", "0x... (leg 1)"] },
          { "name": "encryptedLegProof", "required": true }
        ]
      },
      "exit": {
        "fields": [
          { "name": "outputToken", "required": false, "options": ["..."] },
          { "name": "encryptedLegAmounts", "required": false, "options": ["0x... (redeem batcher, leg 0)", "0x... (leg 1)"] },
          { "name": "encryptedLegProof", "required": false },
          { "name": "encryptedAmount", "required": false, "options": ["0x... (confidential share token)"] },
          { "name": "encryptedAmountProof", "required": false }
        ]
      }
    }
  },
  "executionContracts": {
    "enter": ["0x... (confidential token)", "0x... (router)", "0x... (this vault's deposit batcher)"],
    "exit": ["0x... (router)", "0x... (share token, one per leg)", "0x... (this vault's redeem batcher)"]
  },
  "investmentSchedule": {
    "timezone": "UTC",
    "subscription": { "paths": [{ "cadence": "daily_cutoff", "cutoffTime": "15:00" }] },
    "redemption": { "paths": [{ "cadence": "daily_cutoff", "cutoffTime": "11:00" }] }
  }
}
```

Read every contract address from the yield object. Never hardcode one.

| What you need | Where to read it |
| - | - |
| Plain token (for example USDC) | `token.address` |
| Confidential token (for example cUSDC) | `mechanics.arguments.enter` field `encryptedAmount`, `options[0]` |
| Confidential share token, for an instant exit | `mechanics.arguments.exit` field `encryptedAmount`, `options[0]` |
| Leg list, in order | `encryptedLegAmounts` `options` on `enter` (deposit batchers) or `exit` (redeem batchers) |
| This vault's leg | The leg option that also appears in `executionContracts.enter` (or `.exit` for exits) |
| Router | The entry in `executionContracts.enter` that is neither a leg nor a token in `tokens` |

`status.enter` and `status.exit` turn `false` while the vault's own batcher is paused. Check them before you offer the vault.

The helper below collects these addresses. Later steps use it:

```typescript theme={null}
type Intent = 'enter' | 'exit';
const same = (a: string, b: string) => a.toLowerCase() === b.toLowerCase();

function zamaContracts(yieldData: any, intent: Intent) {
  const fields: { name: string; options?: string[] }[] = yieldData.mechanics.arguments[intent].fields;
  const field = (name: string) => fields.find((f) => f.name === name);

  const legs = field('encryptedLegAmounts')?.options ?? [];
  const own = yieldData.executionContracts?.[intent] ?? [];
  const ownIndex = legs.findIndex((leg) => own.some((c: string) => same(c, leg)));

  const tokenAddresses = [...(yieldData.inputTokens ?? []), ...yieldData.tokens]
    .map((t: { address?: string }) => t.address)
    .filter(Boolean) as string[];
  const router = (yieldData.executionContracts?.enter ?? []).find(
    (c: string) => !legs.some((leg) => same(leg, c)) && !tokenAddresses.some((t) => same(t, c)),
  );

  return {
    legs,
    ownIndex,
    ownLeg: legs[ownIndex],
    router: router as `0x${string}`,
    amountContract: field('encryptedAmount')?.options?.[0] as `0x${string}`,
  };
}
```

### 2. Get the confidential token

A deposit takes the confidential token only. To deposit a plain token, wrap it first. Wrapping happens outside the Yield.xyz API, on the confidential token contract, in up to three transactions:

1. If the wallet's allowance for the confidential token is non-zero but too low, reset it to zero. USDT, for example, reverts when one non-zero allowance changes to another.
2. Approve the confidential token to spend the plain amount.
3. Call `wrap(to, amount)` on the confidential token.

Wrapping is one to one in value. The wrapped amount is `amount / rate()`. `rate()` converts the plain token's decimals to the confidential token's 6 decimals, so read it to know how much to encrypt in step 4. For USDC, USDT and AUSD the decimals match. For WBTC (8 decimals), the wrapper rounds the amount down to a multiple of `rate()` and refunds the rest. See Zama's [confidential wrapper](https://docs.zama.org/protocol/protocol-apps/confidential-tokens/confidential-wrapper) docs for the contract.

```typescript theme={null}
import { createPublicClient, createWalletClient, custom, erc20Abi, http, parseAbi } from 'viem';
import { mainnet } from 'viem/chains';

const wrapperAbi = parseAbi([
  'function wrap(address to, uint256 amount) returns (bytes32)',
  'function rate() view returns (uint256)',
]);

const publicClient = createPublicClient({ chain: mainnet, transport: http(RPC_URL) });
const walletClient = createWalletClient({ chain: mainnet, transport: custom(window.ethereum) });
const [owner] = await walletClient.getAddresses();

async function wrapIntoConfidential(yieldData: any, amount: bigint): Promise<bigint> {
  const underlying = yieldData.token.address as `0x${string}`;
  const { amountContract: wrapper } = zamaContracts(yieldData, 'enter');

  const allowance = await publicClient.readContract({
    address: underlying, abi: erc20Abi, functionName: 'allowance', args: [owner, wrapper],
  });
  const send = async (hash: Promise<`0x${string}`>) =>
    publicClient.waitForTransactionReceipt({ hash: await hash });

  if (allowance < amount) {
    if (allowance > 0n) {
      await send(walletClient.writeContract({
        account: owner, address: underlying, abi: erc20Abi, functionName: 'approve', args: [wrapper, 0n],
      }));
    }
    await send(walletClient.writeContract({
      account: owner, address: underlying, abi: erc20Abi, functionName: 'approve', args: [wrapper, amount],
    }));
  }
  await send(walletClient.writeContract({
    account: owner, address: wrapper, abi: wrapperAbi, functionName: 'wrap', args: [owner, amount],
  }));

  const rate = await publicClient.readContract({ address: wrapper, abi: wrapperAbi, functionName: 'rate' });
  return amount / rate; // confidential token base units, the value to encrypt
}
```

Skip this step if the wallet already holds the confidential token.

The Earn API does not unwrap a confidential token held in the wallet, such as cUSDC back to USDC. To leave a vault in the plain token, use an [instant exit](#9-exit-instantly), which unwraps the share token for you.

### 3. Set up the SDK

Create one SDK instance per wallet and chain, pointed at the Yield.xyz relayer proxy. The proxy accepts your Yield.xyz API key and handles the Zama side, so you need no Zama key. It accepts chain ids `1` and `11155111`, and only the Zama vault contracts that Yield.xyz lists. Zama's [authentication guide](https://docs.zama.org/protocol/sdk/guides/authentication) explains how the SDK uses `relayerUrl` and `auth`.

The SDK asks your signer for EIP-712 signatures when it decrypts. It never sends transactions, because those come from the Yield.xyz API.

```typescript theme={null}
import { BaseSigner, createConfig, memoryStorage, ZamaSDK, type EIP712TypedData } from '@zama-fhe/sdk';
import { chains, type FheChain } from '@zama-fhe/sdk/chains';
import { ViemProvider } from '@zama-fhe/sdk/viem';
import { web } from '@zama-fhe/sdk/web';

const CHAIN_ID = 1; // 11155111 for Ethereum Sepolia

class PermitSigner extends BaseSigner {
  signTypedData(typedData: EIP712TypedData) {
    // The SDK builds the domain chainId as a bigint. Wallets expect a number.
    const { domain, ...rest } = typedData;
    return walletClient.signTypedData({
      account: owner,
      ...rest,
      domain: { ...domain, ...(domain.chainId === undefined ? {} : { chainId: Number(domain.chainId) }) },
    } as Parameters<typeof walletClient.signTypedData>[0]);
  }
  writeContract(): Promise<`0x${string}`> {
    return Promise.reject(new Error('Send Zama transactions through the Yield.xyz API'));
  }
}

const chain = {
  ...chains[CHAIN_ID],
  auth: { __type: 'ApiKeyHeader', value: YIELD_API_KEY },
  relayerUrl: `https://api.yield.xyz/v1/zama/relayer/${CHAIN_ID}/`,
} satisfies FheChain;

const sdk = new ZamaSDK(
  createConfig({
    chains: [chain],
    provider: new ViemProvider({ publicClient }),
    relayers: { [chain.id]: web() },
    signer: new PermitSigner({ address: owner, chainId: CHAIN_ID }),
    storage: memoryStorage, // keeps keys and decrypted values in memory only
  }),
);
```

Create a separate instance for each connected wallet, and call `sdk.terminate()` when you are done with one.

`web()` runs encryption in a Web Worker that it loads from the package, and falls back to the main thread with a console warning if your bundler cannot load that worker. If encryption freezes the page, copy `dist/esm/encrypt.worker.js` from `@zama-fhe/sdk` into your static directory and pass its path: `web({ offloadWorker: '/encrypt.worker.js' })`. A Content Security Policy then needs `worker-src 'self'`.

To check the wallet's confidential token balance before a deposit, decrypt it with `sdk.createToken(address).balanceOf(owner)`. The SDK asks the owner to sign a permit the first time.

```typescript theme={null}
const { amountContract: cToken } = zamaContracts(yieldData, 'enter');
const confidentialBalance = await sdk.createToken(cToken).balanceOf(owner); // bigint, base units
```

### 4. Encrypt the amount and the legs

A deposit needs two encrypted bundles, made for the depositing wallet:

| Bundle | Contract it is encrypted for | Values |
| - | - | - |
| Amount | The confidential token (`encryptedAmount` options) | One `euint64`: the deposit amount |
| Legs | The router | One `euint64` per leg, in option order: the amount for this vault's leg, `0` for every other leg |

```typescript theme={null}
async function encryptDeposit(yieldData: any, value: bigint) {
  const { legs, ownIndex, router, amountContract } = zamaContracts(yieldData, 'enter');

  const [amount, legBundle] = await Promise.all([
    sdk.encrypt({
      contractAddress: amountContract,
      userAddress: owner,
      values: [{ type: 'euint64', value }],
    }),
    sdk.encrypt({
      contractAddress: router,
      userAddress: owner,
      values: legs.map((_, index) => ({ type: 'euint64', value: index === ownIndex ? value : 0n })),
    }),
  ]);

  return {
    encryptedAmount: amount.encryptedValues[0],
    encryptedAmountProof: amount.inputProof,
    encryptedLegAmounts: legBundle.encryptedValues,
    encryptedLegProof: legBundle.inputProof,
  };
}
```

Encrypt again for every new request. A handle made for another wallet or another contract makes the transaction revert.

Zama's [Encrypt and decrypt](https://docs.zama.org/protocol/sdk/guides/encrypt-decrypt) guide covers `sdk.encrypt`, user decryption with a permit (step 7) and public decryption (step 9) in more depth.

### 5. Enter

Send the four encrypted arguments to `POST /v1/actions/enter`. `inputToken` is optional. If you send it, it must be the confidential token.

```bash cURL theme={null}
curl -s -X POST "https://api.yield.xyz/v1/actions/enter" \
  -H "x-api-key: $YIELD_API_KEY" -H "content-type: application/json" \
  -d '{
    "yieldId": "'"$YIELD_ID"'",
    "address": "'"$ADDR"'",
    "arguments": {
      "encryptedAmount": "0x<32-byte handle>",
      "encryptedAmountProof": "0x<proof>",
      "encryptedLegAmounts": ["0x<handle for leg 0>", "0x<handle for leg 1>"],
      "encryptedLegProof": "0x<proof>"
    }
  }'
```

```typescript TypeScript theme={null}
const value = await wrapIntoConfidential(yieldData, 100_000_000n); // 100 USDC
const res = await fetch('https://api.yield.xyz/v1/actions/enter', {
  method: 'POST',
  headers: { 'x-api-key': YIELD_API_KEY, 'content-type': 'application/json' },
  body: JSON.stringify({
    yieldId: yieldData.id,
    address: owner,
    arguments: await encryptDeposit(yieldData, value),
  }),
});
const action = await res.json(); // 201 Created
```

```json Response (excerpt) theme={null}
{
  "id": "action_123abc",
  "intent": "enter",
  "status": "CREATED",
  "amount": null,
  "amountRaw": null,
  "amountUsd": null,
  "isEncrypted": true,
  "encryptedAmount": "0x...",
  "transactions": [
    { "id": "tx_123abc", "stepIndex": 0, "type": "DEPOSIT", "status": "CREATED", "unsignedTransaction": "..." }
  ]
}
```

One `DEPOSIT` transaction comes back. It transfers the encrypted amount to the router, which sends each leg to its vault's batch. `amount`, `amountRaw` and `amountUsd` are `null`, and `encryptedAmount` carries the handle.

Never send `amount`, `amountRaw`, `shareAmount`, `shareAmountRaw` or `useMaxAmount` to a Zama yield. The API rejects them.

### 6. Sign, broadcast and submit the hash

Run each transaction through the same loop as any other yield, in `stepIndex` order: sign `unsignedTransaction`, broadcast it, report the hash with `PUT /v1/transactions/{transactionId}/submit-hash`, then poll `GET /v1/transactions/{transactionId}` until the status is terminal. See [Enter and exit flows](https://docs.yield.xyz/guides/earn/enter-exit-flows.md) for the full loop and [Transaction signing](https://docs.yield.xyz/guides/transaction-signing/evm.md) for EVM signing code.

```typescript theme={null}
for (const tx of action.transactions.sort((a: any, b: any) => a.stepIndex - b.stepIndex)) {
  const hash = await signAndBroadcast(tx.unsignedTransaction);
  await fetch(`https://api.yield.xyz/v1/transactions/${tx.id}/submit-hash`, {
    method: 'PUT',
    headers: { 'x-api-key': YIELD_API_KEY, 'content-type': 'application/json' },
    body: JSON.stringify({ hash }),
  });
  await pollToTerminal(tx.id); // from Enter and exit flows
}
```

When the `DEPOSIT` confirms, the deposit waits in the current batch. It starts earning once that batch settles.

### 7. Read and reveal balances

```bash cURL theme={null}
curl -s -X POST "https://api.yield.xyz/v1/yields/$YIELD_ID/balances" \
  -H "x-api-key: $YIELD_API_KEY" -H "content-type: application/json" \
  -d '{ "address": "'"$ADDR"'" }'
```

```json Response (excerpt) theme={null}
{
  "balances": [
    {
      "type": "active",
      "amount": "",
      "amountRaw": "",
      "amountUsd": null,
      "isEncrypted": true,
      "shareAmountRaw": "0x<handle>",
      "shareToken": { "symbol": "csteakcUSDC", "address": "0x..." },
      "pendingActions": []
    },
    {
      "type": "entering",
      "amount": "",
      "isEncrypted": true,
      "shareAmountRaw": "0x<handle>",
      "token": { "symbol": "cUSDC", "address": "0x..." },
      "date": "2026-10-28T15:00:00.000Z",
      "pendingActions": [
        { "type": "CANCEL_PENDING", "passthrough": "..." }
      ]
    }
  ]
}
```

Every Zama balance has `isEncrypted: true`, an empty `amount`, `amountUsd` set to `null`, and the encrypted handle in `shareAmountRaw`.

| `type` | What it is | Revealed value is in |
| - | - | - |
| `active` | Confidential vault shares the wallet holds | Share token base units |
| `entering` | A deposit waiting in a batch | Confidential token base units |
| `exiting` | A private exit waiting in a batch | Share token base units |
| `withdrawable` | An instant exit to finish. See [step 9](#9-exit-instantly). | Not revealed: it is publicly decrypted |

On `entering` and `exiting` rows, `date` is when the batch can next settle. Pending rows appear for batches the wallet joined through Yield.xyz actions.

To reveal, decrypt the handles with the owner's permit. Each handle is decrypted against its own contract: the share token for `active`, this vault's deposit leg for `entering`, and this vault's redeem leg for `exiting`.

```typescript theme={null}
function handleContract(yieldData: any, balance: any): `0x${string}` {
  if (balance.type === 'entering') return zamaContracts(yieldData, 'enter').ownLeg as `0x${string}`;
  if (balance.type === 'exiting') return zamaContracts(yieldData, 'exit').ownLeg as `0x${string}`;
  return balance.shareToken.address;
}

const revealable = balances.filter((b: any) => b.isEncrypted && b.type !== 'withdrawable');
const clear = await sdk.decryption.decryptValues(
  revealable.map((b: any) => ({
    contractAddress: handleContract(yieldData, b),
    encryptedValue: b.shareAmountRaw,
  })),
);
// clear maps each handle to a bigint in base units. Compare handles in lowercase.
```

The first decrypt asks the owner to sign an EIP-712 permit. To show an `active` balance in the plain token, multiply the share amount by `state.pricePerShareState.price` on the yield.

Revealed amounts belong to the owner. Keep them in memory, and clear them when the wallet changes.

> [!NOTE]
>
> A deposit or a private withdrawal sends an encrypted zero to every other vault of the same asset. Those vaults can show an encrypted balance for the wallet that reveals as zero. Your app can drop a position once it reveals as zero, as the Zama widget does.

### 8. Exit privately

A private exit redeems shares in the next redemption batch and pays out the confidential token. Leave `outputToken` out, or set it to the confidential token, and send legs for the redeem batchers. The legs carry the share amount, encrypted for the router, with `0` for every other leg. A private exit sends no `encryptedAmount`.

```typescript theme={null}
async function encryptPrivateExit(yieldData: any, shares: bigint) {
  const { legs, ownIndex, router } = zamaContracts(yieldData, 'exit');
  const bundle = await sdk.encrypt({
    contractAddress: router,
    userAddress: owner,
    values: legs.map((_, index) => ({ type: 'euint64', value: index === ownIndex ? shares : 0n })),
  });
  return { encryptedLegAmounts: bundle.encryptedValues, encryptedLegProof: bundle.inputProof };
}

const res = await fetch('https://api.yield.xyz/v1/actions/exit', {
  method: 'POST',
  headers: { 'x-api-key': YIELD_API_KEY, 'content-type': 'application/json' },
  body: JSON.stringify({
    yieldId: yieldData.id,
    address: owner,
    arguments: await encryptPrivateExit(yieldData, shares), // shares: revealed active balance or less
  }),
});
```

The transactions come back in this order:

| Type | When | What it does |
| - | - | - |
| `SET_OPERATOR` | Once per share token, only while the grant is missing | Lets the router move that share token for the wallet. The grant does not expire, so later exits skip it. |
| `REDEEM` | Always | Sends each leg to its vault's redemption batch |

The first private exit can return several `SET_OPERATOR` transactions, one for each vault of the same asset. Sign them before the `REDEEM`. When the batch settles, the wallet receives the confidential token. There is nothing to claim.

### 9. Exit instantly

An instant exit skips the batch and pays out the plain token. Its amount becomes public on-chain.

Set `outputToken` to the plain token (`token.address`) and send `encryptedAmount` and `encryptedAmountProof`, encrypted for the confidential share token (the exit `encryptedAmount` option):

```typescript theme={null}
const { amountContract: shareToken } = zamaContracts(yieldData, 'exit');
const encrypted = await sdk.encrypt({
  contractAddress: shareToken,
  userAddress: owner,
  values: [{ type: 'euint64', value: shares }],
});

const res = await fetch('https://api.yield.xyz/v1/actions/exit', {
  method: 'POST',
  headers: { 'x-api-key': YIELD_API_KEY, 'content-type': 'application/json' },
  body: JSON.stringify({
    yieldId: yieldData.id,
    address: owner,
    arguments: {
      outputToken: yieldData.token.address,
      encryptedAmount: encrypted.encryptedValues[0],
      encryptedAmountProof: encrypted.inputProof,
    },
  }),
});
```

One `UNWRAP` transaction comes back. It burns the shares and marks the burned amount for public decryption. Once it confirms, the balances show a `withdrawable` row with a `WITHDRAW` pending action:

```json Balance (excerpt) theme={null}
{
  "type": "withdrawable",
  "isEncrypted": true,
  "shareAmountRaw": "0x<handle>",
  "pendingActions": [
    {
      "type": "WITHDRAW",
      "passthrough": "...",
      "arguments": { "fields": [ { "name": "decryptedAmount", "required": true }, { "name": "decryptionProof", "required": true } ] }
    }
  ]
}
```

Publicly decrypt the row's handle, then send the cleartext and the proof to `POST /v1/actions/manage` with the pending action's `passthrough` unchanged. A public decrypt needs no signature.

```typescript theme={null}
const row = balances.find((b: any) => b.type === 'withdrawable' && b.isEncrypted);
const pending = row.pendingActions.find((p: any) => p.type === 'WITHDRAW');

const handle = row.shareAmountRaw as `0x${string}`;
const { clearValues, decryptionProof } = await sdk.decryption.decryptPublicValues([handle]);
const cleartext = clearValues[handle];
if (typeof cleartext !== 'bigint') throw new Error('Public decrypt returned no value for this handle');

const res = await fetch('https://api.yield.xyz/v1/actions/manage', {
  method: 'POST',
  headers: { 'x-api-key': YIELD_API_KEY, 'content-type': 'application/json' },
  body: JSON.stringify({
    yieldId: yieldData.id,
    address: owner,
    action: 'WITHDRAW',
    passthrough: pending.passthrough,
    arguments: {
      decryptedAmount: cleartext.toString(), // uint64, base units
      decryptionProof,
    },
  }),
});
```

Two transactions come back: `WITHDRAW` finishes the unwrap, then `REDEEM` redeems the vault shares for the plain token. Sign them in order.

If a `WITHDRAW` confirmed but its `REDEEM` did not, a `withdrawable` row with a plain `amount` and no `isEncrypted` appears. Its `WITHDRAW` pending action needs no arguments and returns one `REDEEM`.

### 10. Cancel a pending request

`entering` and `exiting` rows carry a `CANCEL_PENDING` pending action until their batch is sent to the vault. Send it to `POST /v1/actions/manage` with the `passthrough` copied from the balance:

```bash cURL theme={null}
curl -s -X POST "https://api.yield.xyz/v1/actions/manage" \
  -H "x-api-key: $YIELD_API_KEY" -H "content-type: application/json" \
  -d '{
    "yieldId": "'"$YIELD_ID"'",
    "address": "'"$ADDR"'",
    "action": "CANCEL_PENDING",
    "passthrough": "PASSTHROUGH_FROM_THE_BALANCE"
  }'
```

One `CANCEL_PENDING` transaction comes back. It refunds everything the wallet has in that batch, including deposits made outside Yield.xyz, such as in the Zama app. A canceled deposit returns the confidential token. A canceled exit returns the share token.

While a cancel is broadcast but not yet confirmed, the row stays and its `CANCEL_PENDING` action is not offered. The row disappears once the cancel confirms.

## Reward rate

The yield's reward rate is the vault's base APY. The Sepolia test vault reports no reward rate.

Zama runs separate incentive campaigns that holders opt into in the [Zama app](https://app.zama.org). The API does not perform that opt-in, and the reward rate does not include those incentives.

## Errors and troubleshooting

API errors come back with the error name and a reason in the response body.

| Error or symptom | Cause | Fix |
| - | - | - |
| `400` `InvalidArgumentError`: `Confidential vaults take encrypted amounts only; send encryptedLegAmounts (and encryptedAmount on enter) from the Zama SDK.` | The request has `amount`, `shareAmount`, `shareAmountRaw` or `useMaxAmount: true`. | Send only the encrypted arguments. |
| `400` `MissingArgumentsError` naming `encryptedAmount, encryptedAmountProof` or `encryptedLegAmounts, encryptedLegProof` | An enter without the amount or the legs, a private exit without legs, or an instant exit without the amount. | Send both values of each pair from the same SDK call. |
| `400` `InvalidArgumentError`: `encryptedLegAmounts needs one handle per leg: expected N, got M` | The leg count differs from the `encryptedLegAmounts` options. | Encrypt one value per option, in option order, including the zeros. Read the options from the yield again. |
| `400`: `<method> would revert: <reason>. Check the encrypted inputs were made for this wallet and the documented contract, and that the batch is open.` | The handles were encrypted for another wallet or contract, or the batch is closed. | Encrypt again with `userAddress` set to the signing wallet, the amount for the confidential token (or the share token on an instant exit) and the legs for the router. If the inputs are right, retry after the batch opens. |
| `412` `StakeTemporarilyDisabledError` or `UnstakeTemporarilyDisabledError` | This vault's deposit or redeem batcher is paused. `status.enter` or `status.exit` is `false`. | Check `status` before you offer the action, and try again later. |
| `400`: `decryptedAmount must be a uint64 in base units`, or `Invalid unwrap finalization: decryptedAmount must be a uint64 in base units` | The `WITHDRAW` cleartext is not a base-unit integer string. | Send the public decrypt's clear value as a decimal string, unchanged. |
| `400`: `Invalid CANCEL_PENDING passthrough` or `Invalid WITHDRAW passthrough` | The `passthrough` was changed or built by hand. | Copy it from the balance response. |
| The node rejects the `DEPOSIT` for insufficient funds | The wallet cannot pay the gas limit at the maximum fee, even if the deposit would cost less. | Add ETH. Each leg needs at least 1,500,000 gas. |
| `400` from the relayer: `<address> is not a Zama vault contract on chain <chainId>` | The SDK call names a contract outside the Zama vaults that Yield.xyz lists. | Encrypt and decrypt only against the contracts read from the yield object. |
| `400` from the relayer: `Unsupported chainId <chainId>` | The relayer URL has a chain id other than `1` or `11155111`. | Use `1` for Ethereum or `11155111` for Sepolia. |
| A pending row stays after a cancel | The cancel is broadcast but not confirmed. | Wait for the `CANCEL_PENDING` transaction to confirm. The row has no `CANCEL_PENDING` action meanwhile. |

## Next steps

> **[Zama widget](https://docs.yield.xyz/guides/zama/widget.md)**
>
> Embed confidential vaults with the Yield.xyz widget.

> **[Zama vaults](https://docs.yield.xyz/documentation/earn/defi/vaults/zama.md)**
>
> How confidential deposits, batches and exits work.

> **[Enter and exit flows](https://docs.yield.xyz/guides/earn/enter-exit-flows.md)**
>
> The full sign, submit and poll loop.

> **[Transaction signing](https://docs.yield.xyz/guides/transaction-signing/evm.md)**
>
> EVM signing code for the transactions the API returns.

> **[Zama documentation](https://docs.zama.org/protocol/sdk/)**
>
> Zama's SDK docs: configuration, encryption, decryption and confidential tokens.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.