Skip to main content
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

Implementation

1

Set up the webhook

Create a webhook that listens for deposit events. You can do this via the dashboard or API:
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.
2

Detect incoming deposits

When a deposit arrives, DFNS sends a wallet.blockchainevent.detected event. Filter for incoming transfers:
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,
  })
})
See webhook signature verification for the implementation of verifyDfnsWebhookSignature.
Always respond with 200 immediately and process the sweep asynchronously. See webhook best practices.
The sweep itself lands in your treasury wallet, which also fires a wallet.blockchainevent.detected event with direction: "In". If your webhook covers the treasury wallet, filtering on direction === 'In' alone re-enqueues the sweep as a new deposit. Exclude these by skipping events whose walletId is your treasury, or whose from address is one of your own deposit wallets.
3

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.
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}`)
  }
}
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.
4

Track sweep status

Monitor sweep results via the webhook events you subscribed to in step 1:
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
}

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: Use a fee sponsor 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.
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 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.

Sweeping the full balance

The Transfer API requires an explicit amount, and there is no “send max” flag. To drain a wallet completely you must compute amount = balance − fee before transferring, because the fee is deducted from the same balance.
  • Token sweeps (ERC-20, SPL, TRC-20, etc.): the gas fee is paid in the native token, not the token you’re sweeping. Sweep the full token balance as amount, and cover gas separately with a fee sponsor or by pre-funding the wallet with native tokens (see Gas fees).
  • Native-token sweeps on account-based chains (EVM, Solana, TRON, etc.): estimate the fee with Estimate fees, then transfer balance − estimatedFee. Leave a small margin so the transfer doesn’t fail if the fee rises between estimation and broadcast.
  • UTXO chains (Bitcoin, Litecoin, Dogecoin): the fee depends on the number of inputs and outputs. List the wallet’s balance, estimate the fee for a single-output transaction with Estimate fees, and transfer balance − fee. A full-balance transfer leaves no change output. Note that full-balance UTXO transfers cannot be sped up afterward.
For high-throughput systems, prefer leaving a small dust buffer and reconciling periodically over chasing an exact-zero balance. Fees move between estimation and broadcast, and an over-tight amount fails the transfer.

Production considerations

  • Verify webhook signatures: Confirm events are from DFNS. See signature verification.
  • 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 to catch deposits your webhook may have missed.
  • Policies: Add 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. For Tier-2 networks, poll wallet history instead.

Set up webhooks

Setup, verification, and best practices

Create transfers via API

Transfer API usage and asset types

Fee sponsors

Sponsor gas fees for your wallets

Automate deposits

Deposit detection and compliance
Last modified on June 8, 2026