> 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.

# Progress (/api-design/progress)

A payment takes a few seconds, and most of that is the proof. Pass `onProgress` to show your user
what is happening meanwhile.

## How onProgress works

Every payment function takes an `onProgress` option. The SDK calls it once per stage, with an event
whose `stage` names it. The event also carries what that stage produced, such as the fees or a
transaction signature.

If your callback throws, the SDK drops the error and carries on. If it returns a promise, the SDK
does not wait for it. Your callback cannot stop a payment.

## Every stage

The messages are the ones the [example app](https://sdk.nimbusprivacy.xyz/resources/example-app) shows.

| Stage               | Reported by                                      | Message to show                                                             |
| ------------------- | ------------------------------------------------ | --------------------------------------------------------------------------- |
| `synced`            | deposit, Private Send, withdraw                  | Brought this device up to date with Solana.                                 |
| `prepared`          | every payment                                    | Prepared on this device.                                                    |
| `endorsed`          | register, deposit                                | The committee approved the transaction.                                     |
| `proved`            | register, deposit, Private Send, withdraw        | The privacy work on this device finished.                                   |
| `funding-signed`    | deposit, from a wallet that signs only version 0 | Your wallet signed the transfer that funds the deposit.                     |
| `funding-sent`      | deposit, from a wallet that signs only version 0 | Sent the funding transfer to Solana.                                        |
| `funding-confirmed` | deposit, from a wallet that signs only version 0 | The funding transfer landed.                                                |
| `signed`            | register, deposit, refund                        | Your wallet signed the transaction.                                         |
| `sent`              | register, deposit, refund                        | Sent the transaction to Solana.                                             |
| `confirmed`         | register, deposit, refund                        | The transaction landed on Solana.                                           |
| `merge-prepared`    | Private Send, withdraw                           | Combining part of your private balance so one payment can cover the amount. |
| `merge-relayed`     | Private Send, withdraw                           | The relayer accepted the combining step.                                    |
| `merge-settled`     | Private Send, withdraw                           | The combining step finished.                                                |
| `relayed`           | Private Send, withdraw                           | The relayer accepted the transaction and will send it.                      |
| `settled`           | Private Send, withdraw                           | Followed the payment until Solana settled it.                               |

"Refund" here is `refundStrandedDeposit`. A few stages do not always come, or come in a different
order:

- `endorsed` and `proved` run at the same time, so they can arrive in either order.
- A registration with nothing left to send stops after `prepared`.
- A transaction that fails, expires or is still pending stops at `sent`. The result says where it
  ended up.
- The merge stages come before the payment's own, once per merge. Each carries a `merge` number,
  from 1.
- `settled` comes only when the SDK follows the payment, which it does unless you pass
  `follow: false`.

## A message for every stage

Each payment's stages form a union on `stage`. A `switch` over all of them is checked by TypeScript.
If the SDK adds a stage, this stops compiling until you give it a message.

```ts
type PaymentStage =
  | RegisterStage
  | DepositStage
  | TransferStage
  | WithdrawStage
  | RefundStage
  | SyncedStage
  | SettledStage;

function describeStage(event: PaymentStage): string {
  switch (event.stage) {
    case "synced":
      return "Brought this device up to date with Solana.";
    case "prepared":
      return "Prepared on this device.";
    case "endorsed":
      return "The committee approved the transaction.";
    case "proved":
      return "The privacy work on this device finished.";
    case "funding-signed":
      return "Your wallet signed the transfer that funds the deposit.";
    case "funding-sent":
      return "Sent the funding transfer to Solana.";
    case "funding-confirmed":
      return "The funding transfer landed.";
    case "signed":
      return "Your wallet signed the transaction.";
    case "sent":
      return "Sent the transaction to Solana.";
    case "confirmed":
      return "The transaction landed on Solana.";
    case "merge-prepared":
      return `Combining part of your private balance, step ${event.merge}.`;
    case "merge-relayed":
      return `The relayer accepted combining step ${event.merge}.`;
    case "merge-settled":
      return `Combining step ${event.merge} finished.`;
    case "relayed":
      return "The relayer accepted the transaction and will send it.";
    case "settled":
      return "Followed the payment until Solana settled it.";
  }
}
```

## A progress list in React

This component runs a deposit and lists each stage as it arrives:

```tsx
import { useState } from "react";
import type { ReactElement } from "react";
import { deposit } from "@umbra/nimbus-solana-web";

export function DepositWithProgress({
  context,
  usdc,
}: {
  context: DepositContext;
  usdc: Address;
}): ReactElement {
  const [lines, setLines] = useState<readonly string[]>([]);
  const [running, setRunning] = useState(false);

  async function run(): Promise<void> {
    setLines([]);
    setRunning(true);
    try {
      await deposit(
        context,
        {
          mint: usdc,
          amount: 25_000_000n,
          recipient: context.session.walletAddress,
        },
        {
          onProgress: (event) => {
            setLines((current) => [...current, describeStage(event)]);
          },
        },
      );
    } finally {
      setRunning(false);
    }
  }

  return (
    <div>
      <button type="button" disabled={running} onClick={() => void run()}>
        Deposit 25 USDC
      </button>
      {running && <p>Keep this tab open until the deposit finishes.</p>}
      <ol>
        {lines.map((line, index) => (
          <li key={index}>{line}</li>
        ))}
      </ol>
    </div>
  );
}
```

The button stays disabled while the deposit runs, so your user cannot start a second one. The
result's `kind` still says how it ended. Show it as [the deposit guide](https://sdk.nimbusprivacy.xyz/deposit) does.

- [How a payment works](https://sdk.nimbusprivacy.xyz/explainers/payments): What each stage stands for.
- [Results and errors](https://sdk.nimbusprivacy.xyz/api-design/results-and-errors): What to show when a payment ends.


Last updated 21 October 2026
