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

# Sweep deposits

> Automatically sweep incoming deposits to a treasury wallet using Dfns webhooks, balances, and the transfer API for high-volume custody operations.

This guide walks through building an automated deposit sweep. Whenever funds land in a deposit wallet, your system detects the deposit and transfers the funds to a central treasury wallet.

## How it works

```mermaid theme={null}
sequenceDiagram
    participant Network as Blockchain
    participant Dfns
    participant System as Your backend

    Network->>Dfns: Confirmed deposit detected
    Dfns->>System: Webhook (wallet.blockchainevent.detected)
    System->>System: Validate and record deposit
    System->>Dfns: Create transfer to treasury wallet
    Dfns->>Network: Sweep transaction broadcast
    Dfns->>System: Webhook (wallet.transfer.confirmed)
```

## Implementation

<Steps>
  <Step title="Set up the webhook">
    Create a webhook that listens for deposit events. You can do this via the [dashboard](/guides/developers/webhooks) or [API](/api-reference/webhooks/create-webhook):

    ```typescript theme={null}
    const webhook = await dfns.webhooks.createWebhook({
      body: {
        url: 'https://your-app.com/webhooks/dfns',
        events: [
          'wallet.blockchainevent.detected',
          'wallet.transfer.confirmed',
          'wallet.transfer.failed',
        ],
        status: 'Enabled',
      },
    })

    // Store the webhook secret. You'll need it to verify signatures
    console.log('Secret:', webhook.secret)
    ```

    Subscribe to `wallet.transfer.confirmed` and `wallet.transfer.failed` too, so you can track the sweep transfer status.
  </Step>

  <Step title="Detect incoming deposits">
    When a deposit arrives, Dfns sends a `wallet.blockchainevent.detected` event. Filter for incoming transfers:

    <CodeGroup>
      ```typescript TypeScript (Express) theme={null}
      app.post('/webhooks/dfns', express.json(), async (req, res) => {
        const event = req.body

        // Verify the event comes from Dfns. See webhook signature verification
        if (!verifyDfnsWebhookSignature(event, req.headers['x-dfns-webhook-signature'])) {
          return res.status(401).send('Invalid signature')
        }

        // Always respond quickly. Process async
        res.status(200).send('OK')

        if (event.kind !== 'wallet.blockchainevent.detected') return

        const { walletId, blockchainEvent } = event.data

        // Only process incoming transfers
        if (blockchainEvent.direction !== 'In') return

        await enqueueSweep({
          walletId,
          kind: blockchainEvent.kind,
          amount: blockchainEvent.value,
          contract: blockchainEvent.contract, // present for token transfers
          txHash: blockchainEvent.txHash,
        })
      })
      ```

      ```python Python (Flask) theme={null}
      @app.route('/webhooks/dfns', methods=['POST'])
      def handle_webhook():
          event = request.json

          # Verify the event comes from Dfns. See webhook signature verification
          if not verify_dfns_webhook_signature(event, request.headers.get("X-DFNS-WEBHOOK-SIGNATURE", "")):
              return "Invalid signature", 401

          if event["kind"] != "wallet.blockchainevent.detected":
              return "OK", 200

          wallet_id = event["data"]["walletId"]
          blockchain_event = event["data"]["blockchainEvent"]

          if blockchain_event["direction"] != "In":
              return "OK", 200

          enqueue_sweep(
              wallet_id=wallet_id,
              kind=blockchain_event["kind"],
              amount=blockchain_event["value"],
              contract=blockchain_event.get("contract"),
              tx_hash=blockchain_event["txHash"],
          )

          return "OK", 200
      ```
    </CodeGroup>

    See [webhook signature verification](/guides/developers/webhooks#verify-events-are-sent-from-dfns) for the implementation of `verifyDfnsWebhookSignature`.

    <Warning>
      Always respond with `200` immediately and process the sweep asynchronously. See [webhook best practices](/guides/developers/webhooks#webhooks-best-practices).
    </Warning>
  </Step>

  <Step title="Execute the sweep transfer">
    From your queue processor, create a transfer from the deposit wallet to your treasury wallet. The transfer `kind` must match the deposit type.

    <CodeGroup>
      ```typescript TypeScript theme={null}
      const TREASURY_WALLET_ID = 'wa-treasury-xxx'

      // Get the treasury wallet address
      const treasury = await dfns.wallets.getWallet({
        walletId: TREASURY_WALLET_ID,
      })

      async function executeSweep(deposit: {
        walletId: string
        kind: string
        amount: string
        contract?: string
        txHash: string
      }) {
        // Map blockchain event kind to transfer kind
        const transferBody = buildTransferBody(deposit, treasury.address)

        const transfer = await dfns.wallets.createTransfer({
          walletId: deposit.walletId,
          body: {
            ...transferBody,
            externalId: `sweep-${deposit.txHash}`, // idempotency
          },
        })

        console.log(`Sweep initiated: ${transfer.id} (${transfer.status})`)
      }

      function buildTransferBody(
        deposit: { kind: string; amount: string; contract?: string },
        treasuryAddress: string
      ) {
        switch (deposit.kind) {
          case 'NativeTransfer':
            return {
              kind: 'Native' as const,
              to: treasuryAddress,
              amount: deposit.amount,
            }
          case 'Erc20Transfer':
            return {
              kind: 'Erc20' as const,
              to: treasuryAddress,
              amount: deposit.amount,
              contract: deposit.contract!,
            }
          case 'SplTransfer':
            return {
              kind: 'Spl' as const,
              to: treasuryAddress,
              amount: deposit.amount,
              contract: deposit.contract!,
            }
          // Handle other transfer kinds as needed
          default:
            throw new Error(`Unsupported transfer kind: ${deposit.kind}`)
        }
      }
      ```

      ```python Python theme={null}
      TREASURY_WALLET_ID = "wa-treasury-xxx"

      treasury = client.wallets.get_wallet(TREASURY_WALLET_ID)

      def execute_sweep(deposit: dict):
          transfer_body = build_transfer_body(deposit, treasury["address"])
          transfer_body["externalId"] = f"sweep-{deposit['txHash']}"  # idempotency

          transfer = client.wallets.create_transfer(
              deposit["wallet_id"],
              body=transfer_body,
          )

          print(f"Sweep initiated: {transfer['id']} ({transfer['status']})")

      def build_transfer_body(deposit: dict, treasury_address: str) -> dict:
          kind = deposit["kind"]

          if kind == "NativeTransfer":
              return {
                  "kind": "Native",
                  "to": treasury_address,
                  "amount": deposit["amount"],
              }
          elif kind == "Erc20Transfer":
              return {
                  "kind": "Erc20",
                  "to": treasury_address,
                  "amount": deposit["amount"],
                  "contract": deposit["contract"],
              }
          elif kind == "SplTransfer":
              return {
                  "kind": "Spl",
                  "to": treasury_address,
                  "amount": deposit["amount"],
                  "contract": deposit["contract"],
              }
          else:
              raise ValueError(f"Unsupported transfer kind: {kind}")
      ```
    </CodeGroup>

    <Tip>
      Use `externalId` with a value derived from the deposit transaction hash. This ensures the same deposit is never swept twice, even if your webhook handler processes the event more than once. See [idempotency](/api-reference/idempotency).
    </Tip>
  </Step>

  <Step title="Track sweep status">
    Monitor sweep results via the webhook events you subscribed to in step 1:

    ```typescript theme={null}
    switch (event.kind) {
      case 'wallet.transfer.confirmed':
        const { transferRequest } = event.data
        console.log(`Sweep confirmed: ${transferRequest.id}`)
        console.log(`Tx hash: ${transferRequest.txHash}`)
        // Mark sweep as complete in your database
        break

      case 'wallet.transfer.failed':
        const { transferRequest: failed } = event.data
        console.error(`Sweep failed: ${failed.id}, ${failed.reason}`)
        // Alert and retry
        break
    }
    ```
  </Step>
</Steps>

## Gas fees

The deposit wallet needs native tokens to pay gas fees for the sweep transaction. This creates a chicken-and-egg problem: the wallet receives a token deposit, but may not have native tokens for gas.

Two options:

### Option 1: Fee sponsor (recommended)

Use a [fee sponsor](/features/fee-sponsors) so the deposit wallet doesn't need to hold native tokens at all. A single funded fee sponsor wallet pays gas for all your sweep transactions.

```typescript theme={null}
const transfer = await dfns.wallets.createTransfer({
  walletId: deposit.walletId,
  body: {
    kind: 'Erc20',
    to: treasuryAddress,
    amount: deposit.amount,
    contract: deposit.contract,
    feeSponsorId: 'fsp-xxx', // your fee sponsor ID
  },
})
```

See [supported networks](/features/fee-sponsors#supported-networks) for fee sponsor availability.

### Option 2: Pre-fund with native tokens

Fund each deposit wallet with a small amount of native tokens when you create it. Top up as needed after sweeps. This works on all networks but requires more operational overhead.

## Batching sweeps

Instead of sweeping immediately after each deposit, you can batch sweeps on a schedule (e.g., every hour or once daily). This is particularly useful for:

* **UTXO networks** (Bitcoin, Litecoin, Dogecoin): batching consolidates many small UTXOs and reduces total fees
* **High-volume systems**: reduces the number of transactions and associated gas costs

To batch, store detected deposits in a queue and process them periodically instead of calling `executeSweep` inline.

## Production considerations

* **Verify webhook signatures**: Confirm events are from Dfns. See [signature verification](/guides/developers/webhooks#verify-events-are-sent-from-dfns).
* **Handle duplicates**: Webhooks may be delivered more than once. Use `externalId` for idempotency and track processed deposits in your database.
* **Reconciliation**: Run periodic reconciliation jobs using [wallet history](/api-reference/wallets/get-wallet-history) to catch deposits your webhook may have missed.
* **Policies**: Add [policies](/core-concepts/policies) to control sweep behavior (e.g., velocity limits, amount thresholds). Service accounts should never have approval authority. Keep automation and oversight separate.
* **Tier-1 networks only**: Deposit detection via `wallet.blockchainevent.detected` is only available on [Tier-1 networks](/networks#tier-1-blockchain-networks). For Tier-2 networks, poll [wallet history](/api-reference/wallets/get-wallet-history) instead.

## Related

<CardGroup cols={2}>
  <Card title="Set up webhooks" icon="webhook" href="/guides/developers/webhooks">
    Setup, verification, and best practices
  </Card>

  <Card title="Create transfers via API" icon="paper-plane" href="/guides/developers/create-transfers">
    Transfer API usage and asset types
  </Card>

  <Card title="Fee sponsors" icon="gas-pump" href="/features/fee-sponsors">
    Sponsor gas fees for your wallets
  </Card>

  <Card title="Automate deposits" icon="money-bill-transfer" href="/solutions/automate-deposits">
    Deposit detection and compliance
  </Card>
</CardGroup>
