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

# Withdrawals

> Recipient-first stablecoin withdrawals with the Web SDK

`DaimoWithdrawal` is a pre-built React flow for sending USDC or USDT to a
recipient. It collects the recipient and destination first, asks your server to
create an open-amount session, then funds that session from an injected wallet
or a host-controlled wallet.

The component provides:

* EVM addresses, Solana addresses, and ENS recipients
* saved recipient routes scoped to the authenticated user
* supported stablecoin and destination-network selection
* wallet token/balance selection and amount entry
* session polling and started/completed lifecycle callbacks

The current destination picker supports USDC on Arbitrum, Base, BNB Smart
Chain, Ethereum, HyperEVM, Optimism, Polygon, and Solana. It supports USDT on
the same EVM networks except HyperEVM. Solana destinations currently support
USDC only.

## Install and provide the client

```bash theme={null}
npm install @daimo/sdk
```

```tsx theme={null}
import "@daimo/sdk/web/theme.css";
import { DaimoSDKProvider } from "@daimo/sdk/web";

export function App({ children }) {
  return <DaimoSDKProvider>{children}</DaimoSDKProvider>;
}
```

## Server callback

ENS resolution is built in. The only required backend callback is
`createSession`, which runs from browser code and must call your authenticated
backend. Keep your Daimo API key on the server.

```ts theme={null}
import type { DaimoWithdrawalDestination } from "@daimo/sdk/web";

async function createSession(input: {
  destination: DaimoWithdrawalDestination;
  fundingMode: "injected-wallet" | "manual";
}) {
  const response = await fetch("/api/withdrawals/session", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(input),
  });
  if (!response.ok) throw new Error("failed to create withdrawal");
  return response.json() as Promise<{
    sessionId: string;
    clientSecret: string;
  }>;
}
```

### ENS resolution

`DaimoWithdrawal` trims and ENSIP-15-normalizes ENS names, then resolves them
through the Daimo API URL configured on `DaimoSDKProvider`. Resolution always
uses Ethereum mainnet, regardless of the selected destination network. Saved
ENS recipients are resolved again before reuse so session creation receives the
same concrete address the user reviewed.

Most integrations should use this zero-configuration default. If your
integration intentionally needs a different resolver, pass `resolveEns`:

```tsx theme={null}
<DaimoWithdrawal
  {...withdrawalProps}
  resolveEns={(name) =>
    fetch(`/api/custom-ens?name=${encodeURIComponent(name)}`).then(
      async (response) => {
        if (!response.ok) throw new Error("failed to resolve ENS name");
        return response.json() as Promise<{ address: `0x${string}` }>;
      },
    )
  }
/>
```

The explicit callback receives the normalized name and takes precedence over
Daimo resolution. Keep paid RPC credentials behind your authenticated backend;
never include them in the browser bundle.

Your session endpoint should validate the destination against
`isDaimoWithdrawalDestination`, then call `POST /v1/sessions` with that
destination and no `amountUnits`. Set `display.paymentOptions` to
`["AutoconnectInjectedWallets"]` for injected-wallet funding or
`["AllAddresses"]` for manual funding. Return only `sessionId` and
`clientSecret` to the component.

<Warning>
  Never place a Daimo API key in the browser bundle. Bind saved-recipient
  `contactStorageScope` to a stable authenticated user or account ID; do not use
  a shared constant.
</Warning>

## Embedded or modal presentation

`DaimoWithdrawal` renders inline by default. Set `embedded={false}` and mount it
only while open to use the SDK's floating modal instead of building another
drawer around the embedded component:

```tsx theme={null}
return (
  <>
    {withdrawalOpen && (
      <DaimoWithdrawal
        {...withdrawalProps}
        embedded={false}
        onClose={() => setWithdrawalOpen(false)}
      />
    )}
  </>
);
```

The modal owns its backdrop and close button. Unmount and remount it to start a
fresh recipient flow.

## Injected-wallet funding

The SDK discovers a browser wallet, shows its token balances, and submits the
selected EVM or Solana transaction. If your app already owns an EVM provider,
pass both the connected address and that scoped provider.

```tsx theme={null}
import { DaimoWithdrawal } from "@daimo/sdk/web";

export function Withdrawal({ user, wallet }) {
  return (
    <DaimoWithdrawal
      fundingMode="injected-wallet"
      walletSource="evm"
      connectToAddress={wallet.address}
      evmProvider={wallet.provider}
      contactStorageScope={user.id}
      createSession={createSession}
      onPaymentStarted={() => console.log("withdrawal submitted")}
      onPaymentCompleted={() => console.log("withdrawal completed")}
    />
  );
}
```

Omit `connectToAddress` and `evmProvider` to let the SDK discover and connect a
wallet. The default `walletSource="all"` includes EVM and Solana funding;
`walletSource="evm"` excludes Solana-only providers.

## Manual funding

Manual funding is for embedded, smart-account, custodial, or sponsored wallets
whose transaction lifecycle belongs to your application. The SDK never calls
`eth_sendTransaction` or any other provider transaction method in manual mode.
It invokes `sendManualTransaction` and waits for your promise instead.

There are three amount variants.

### Fixed amount

Pass a positive decimal `amountUnits`. After destination review, the SDK creates
the open-amount session and immediately invokes the adapter. Fixed amount and
`connectToAddress` are mutually exclusive.

```tsx theme={null}
<DaimoWithdrawal
  fundingMode="manual"
  amountUnits="25.00"
  contactStorageScope={user.id}
  createSession={createSession}
  sendManualTransaction={async ({ receiverAddress, amountUnits }) => {
    const txHash = await treasury.sendUsdc({
      to: receiverAddress,
      amountUnits,
    });
    return { txHash };
  }}
/>
```

### Generic amount entry

Omit both `amountUnits` and `connectToAddress`. The SDK shows a plain USD input
that accepts `$0.01+` with up to two decimal places. It deliberately shows no
token artwork, balance, or Max action. Your adapter owns source network, token,
and decimal-to-raw conversion.

```tsx theme={null}
<DaimoWithdrawal
  fundingMode="manual"
  contactStorageScope={user.id}
  createSession={createSession}
  sendManualTransaction={async ({
    receiverAddress,
    amountUnits,
    expiresAt,
  }) => {
    const txHash = await embeddedWallet.sendUsdc({
      to: receiverAddress,
      amountUnits,
      expiresAt,
    });
    return { txHash };
  }}
/>
```

### Address-aware token and balance selection

Omit `amountUnits` and pass the embedded wallet's EVM address. The SDK uses the
address only to fetch wallet options, shows supported tokens and balances, and
always shows Max on the wallet amount page. It does not connect to the wallet.

```tsx theme={null}
import { isNativeToken } from "@daimo/sdk/common";
import { DaimoWithdrawal } from "@daimo/sdk/web";
import { encodeFunctionData, erc20Abi, getAddress } from "viem";

<DaimoWithdrawal
  fundingMode="manual"
  connectToAddress={embeddedWallet.address}
  sourceTokenFilter={(token) => embeddedWallet.canSend(token)}
  contactStorageScope={user.id}
  createSession={createSession}
  sendManualTransaction={async ({ receiverAddress, source }) => {
    if (!source) throw new Error("source token is required");
    const tokenAddress = getAddress(source.token.token);
    const transaction = isNativeToken(source.token.chainId, tokenAddress)
      ? {
          chainId: source.token.chainId,
          to: receiverAddress,
          value: source.amount,
        }
      : {
          chainId: source.token.chainId,
          to: tokenAddress,
          data: encodeFunctionData({
            abi: erc20Abi,
            functionName: "transfer",
            args: [receiverAddress, source.amount],
          }),
        };
    const txHash = await embeddedWallet.sendTransaction(transaction);
    return { txHash };
  }}
/>;
```

`sourceTokenFilter` removes unsupported balances before the selector renders.
Use it when the host wallet supports only particular chains or token-transfer
commands; do not hide rows with CSS. The adapter must still validate `source`
before submitting because the predicate is a UI constraint, not authorization.
`evmProvider` is not accepted by any manual variant.

<Note>
  World Mini Apps should map every supported World Chain payment token (for
  example USDC, WLD, EURC, WARS, WCOP, WMXN, WBRL, WPEN, and WCLP) to the
  corresponding MiniKit `Tokens` value and call `MiniKit.pay`. A direct ERC-20
  call through `sendTransaction` requires that contract to be allowlisted and
  otherwise fails with `invalid_contract`. Filter out non-World-Chain and
  non-MiniKit tokens with `sourceTokenFilter`.
</Note>

## Manual adapter contract

```ts theme={null}
type DaimoWithdrawalManualTransferRequest = {
  sessionId: string;
  receiverAddress: Address;
  destination: DaimoWithdrawalDestination;
  expiresAt: number;
  amountUnits: string;
  source?: {
    address: Address;
    token: DaimoPayToken;
    amount: bigint;
  };
};

type DaimoWithdrawalManualTransferResult = void | { txHash?: `0x${string}` };
```

`amountUnits` is the exact fixed or user-entered decimal string. In the
address-aware variant, `source` is always present: `source.address` is the
address you supplied, `source.token` contains the selected chain, token
address, symbol, and decimals, and `source.amount` is the exact raw,
balance-capped token amount. In the other variants, `source` is omitted.

Resolve the adapter only after the transaction has been submitted or durably
handed off. Return `txHash` when available so polling can detect it sooner.
Reject only when repeating the same transfer is safe: the retry action invokes
the adapter again for the same session and hidden receiver. Concurrent calls
and calls after a successful adapter result are deduplicated.

## Recipient and session lifecycle

The flow is recipient-first:

1. The user enters an EVM address, Solana address, or ENS name, or selects a
   saved recipient.
2. The user chooses USDC or USDT and a compatible destination network.
3. The SDK asks your server to create an open-amount session.
4. The injected wallet or manual adapter submits funds to the hidden receiver.
5. The SDK polls until the session succeeds, bounces, or expires.

Saved recipients include the identifier, destination asset, and network. ENS
is resolved again before reuse, so the reviewed address stays current. The
component calls `onPaymentStarted` once after submission and
`onPaymentCompleted` once when the session reaches `succeeded`.

Typical session progression is `waiting_payment` → `processing` → `succeeded`.
`bounced` means delivery failed and funds were returned; `expired` means no
transfer was detected before the session deadline. Amount choice does not alter
the session contract: all withdrawal sessions remain open-amount.
