Skip to main content

Overview

After creating orders with affiliate fees, you need to withdraw your accumulated fees once the orders are fulfilled. This guide shows you how to batch withdraw affiliate fees from multiple orders on Solana in a single transaction, minimizing transaction costs.

When to Withdraw

Affiliate fees become available for withdrawal when orders reach the ClaimedUnlock state. The withdrawal process involves:
  1. Querying for unlocked orders with your affiliate address
  2. Building withdrawal instructions for each order with available fees
  3. Batching instructions into optimally-sized transactions
  4. Executing the withdrawal transactions

Prerequisites

  • Orders with affiliate fees in ClaimedUnlock state
  • Solana wallet with SOL for transaction fees
  • Solana RPC URL configured
  • @debridge-finance/solana-utils and @debridge-finance/dln-client packages installed

Complete Implementation

Here’s the complete batch withdrawal script for Solana:
sol-batch-withdraw.ts
import {
  clusterApiUrl,
  ComputeBudgetProgram,
  Connection,
  Keypair,
  MessageV0,
  PACKET_DATA_SIZE,
  PublicKey,
  TransactionInstruction,
  VersionedTransaction
} from "@solana/web3.js";
import { 
  constants, 
  findAssociatedTokenAddress, 
  helpers, 
  spl, 
  txs, 
  programs 
} from "@debridge-finance/solana-utils";
import { ChainId, Solana } from "@debridge-finance/dln-client";
import bs58 from "bs58";
import { getEnvConfig } from "../../utils";

interface IOrderFromApi {
  orderId: { stringValue: string; bytesArrayValue: string };
  affiliateFee: { beneficiarySrc: { stringValue: string } };
  giveOfferWithMetadata: { tokenAddress: { stringValue: string } };
}

// Query unlocked orders with affiliate fees
async function getUnlockedOrders({
  chainIds,
  beneficiaryAddress,
  ref,
}: { 
  chainIds: number[]; 
  beneficiaryAddress: PublicKey; 
  ref?: string; 
}): Promise<IOrderFromApi[]> {
  const MAX = 100;
  let n = 0;
  let lastOrders = [];
  lastOrders.length = MAX;
  let allOrders: IOrderFromApi[] = [];

  try {
    for (; ;) {
      const response = await fetch("https://stats-api.dln.trade/api/Orders/filteredList", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          giveChainIds: chainIds,
          takeChainIds: [],
          orderStates: ["ClaimedUnlock"],  // Only orders ready for withdrawal
          filter: beneficiaryAddress.toString(),
          referralCode: ref,
          skip: n * MAX,
          take: MAX,
        }),
      }).then((r) => r.json());

      n += 1;
      lastOrders = response.orders;
      allOrders = [...allOrders, ...response.orders];

      if (!lastOrders.length) break;
    }
  } catch (e) {
    console.error(e);
  }

  // Filter to only orders where you're the beneficiary
  allOrders = allOrders.filter(
    (order) => order.affiliateFee.beneficiarySrc.stringValue === beneficiaryAddress.toString(),
  );

  return allOrders;
}

// Build withdrawal instruction for a single order
function buildWithdrawAffiliateFeeIx(
  client: Solana.DlnClient, 
  orderId: Buffer, 
  beneficiary: PublicKey, 
  tokenMint: PublicKey, 
  tokenProgram?: PublicKey
) {
  const discriminator = [143, 79, 158, 208, 125, 51, 86, 85];
  tokenProgram = tokenProgram ?? new PublicKey("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
  
  const ix = new TransactionInstruction({
    keys: [
      { isSigner: true, isWritable: true, pubkey: beneficiary },
      { isSigner: false, isWritable: true, pubkey: findAssociatedTokenAddress(beneficiary, tokenMint, tokenProgram)[0] },
      { isSigner: false, isWritable: true, pubkey: client.source.accountsResolver.getGiveOrderStateAccount(orderId)[0] },
      { isSigner: false, isWritable: true, pubkey: client.source.accountsResolver.getGiveOrderWalletAddress(orderId)[0] },
      { isSigner: false, isWritable: false, pubkey: tokenMint },
      { isSigner: false, isWritable: false, pubkey: tokenProgram },
    ],
    programId: client.source.program.programId,
    data: Buffer.concat([Uint8Array.from(discriminator), Uint8Array.from(orderId)]),
  });

  return ix;
}

// Get withdrawal instructions for all orders with available fees
async function getWithdrawAffiliateFeeInstructions(
  client: Solana.DlnClient, 
  orders: IOrderFromApi[]
): Promise<{ instructions: TransactionInstruction[], orderIds: string[] }> {
  const orderIds: string[] = [];
  const instructions: TransactionInstruction[] = [];
  const chunks: PublicKey[][] = [];

  // Get order wallet addresses
  const wallets = orders.map(
    ({ orderId }) =>
      client.source.accountsResolver.getGiveOrderWalletAddress(
        Buffer.from(JSON.parse(orderId.bytesArrayValue))
      )[0],
  );

  // Chunk wallets for batch querying (1000 per request)
  for (let i = 0; i < wallets.length; i += 1000) {
    chunks.push(wallets.slice(i, i + 1000));
  }

  // Get account info for all order wallets
  const accounts = (
    await Promise.all(
      chunks.map((chunk) => client.connection.getMultipleAccountsInfo(chunk))
    )
  ).flat();

  // Build instructions only for orders with available fees
  for (const [i, order] of orders.entries()) {
    const rawAccount = accounts[i]!;
    const account = spl.parseSplAccount(rawAccount.data);

    // Only create instruction if there are fees to withdraw
    if (account?.amount) {
      instructions.push(
        buildWithdrawAffiliateFeeIx(
          client,
          Buffer.from(JSON.parse(order.orderId.bytesArrayValue)), 
          new PublicKey(order.affiliateFee.beneficiarySrc.stringValue), 
          new PublicKey(order.giveOfferWithMetadata.tokenAddress.stringValue),
          rawAccount.owner
        )
      );
      orderIds.push(order.orderId.stringValue);
    }
  }

  return { instructions, orderIds };
}

// Split instructions into optimal transaction sizes
function splitInstructions(
  payer: PublicKey,
  data: { instructions: TransactionInstruction[], orderIds: string[] }
): { ixPacks: TransactionInstruction[][], orderIdsPacks: string[][] } {
  const { instructions, orderIds } = data;

  const defaultArgs = {
    payerKey: payer,
    recentBlockhash: constants.FAKE_BLOCKHASH,
  };

  // Base instructions for compute budget
  const baseInstructions = [
    ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }),
    ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 30_000 }),
  ];

  const ixPacks = [];
  const orderIdsPacks = [];
  let subIxPack = [...baseInstructions];
  let subOrderIdsPacks: string[] = [];

  const compileTransaction = (instructions: TransactionInstruction[]) =>
    new VersionedTransaction(
      MessageV0.compile({
        instructions,
        ...defaultArgs,
      }),
    );

  const checkSize = (instructions: TransactionInstruction[]) =>
    txs.getTransactionSize(compileTransaction(instructions));

  // Pack instructions into transactions that fit in packet size
  for (const [i, instruction] of instructions.entries()) {
    const size = checkSize([...subIxPack, instruction]);

    if (size && size <= PACKET_DATA_SIZE) {
      subIxPack.push(instruction);
      subOrderIdsPacks.push(orderIds[i]);
    } else {
      // Current pack is full, start a new one
      ixPacks.push(subIxPack);
      orderIdsPacks.push(subOrderIdsPacks);
      subIxPack = [...baseInstructions, instruction];
      subOrderIdsPacks = [orderIds[i]];
    }
  }

  if (subIxPack.length > baseInstructions.length) {
    ixPacks.push(subIxPack);
    orderIdsPacks.push(subOrderIdsPacks);
  }

  return { ixPacks, orderIdsPacks };
}

async function main() {
  const { solPrivateKey, solRpcUrl } = getEnvConfig();

  // Your Solana public key (affiliate fee recipient)
  const beneficiaryPubkey = "862oLANNqhdXyUCwLJPBqUHrScrqNR4yoGWGTxjZftKs";

  try {
    Keypair.fromSecretKey(bs58.decode(solPrivateKey));
    new PublicKey(beneficiaryPubkey);
  } catch (err) {
    console.error("Invalid keys provided");
    console.error(err.message);
    process.exit();
  }

  // Set up Solana connection and DLN client
  const connection = new Connection(solRpcUrl ?? clusterApiUrl("mainnet-beta"));
  const client = new Solana.DlnClient(
    connection,
    programs.dlnSrc,
    programs.dlnDst,
    programs.deBridge,
    programs.settings,
  );
  const keypair = Keypair.fromSecretKey(bs58.decode(solPrivateKey));
  const wallet = new helpers.Wallet(keypair);

  // Get all unlocked orders with your affiliate address
  const orders = await getUnlockedOrders({
    chainIds: [ChainId.Solana],
    beneficiaryAddress: new PublicKey(beneficiaryPubkey),
  });

  console.log(`Unclaimed orders: ${orders.length}`);

  if (orders.length === 0) {
    console.log("No orders available for withdrawal");
    return;
  }

  // Build withdrawal instructions
  const ordersData = await getWithdrawAffiliateFeeInstructions(client, orders);
  
  if (ordersData.instructions.length === 0) {
    console.log("No fees available to withdraw");
    return;
  }

  // Split into optimized transaction batches
  const { ixPacks, orderIdsPacks } = splitInstructions(keypair.publicKey, ordersData);

  // Create versioned transactions
  const txs = ixPacks.map(
    (instructions) =>
      new VersionedTransaction(
        MessageV0.compile({
          instructions,
          payerKey: keypair.publicKey,
          recentBlockhash: constants.FAKE_BLOCKHASH,
        }),
      ),
  );

  console.log(`Total instructions: ${ordersData.instructions.length}, total transactions: ${txs.length}`);
  console.log('Withdrawal started...');

  // Execute each transaction
  for (const [i, tx] of txs.entries()) {
    const [id] = await helpers.sendAll(connection, wallet, tx, {
      blockhashCommitment: "finalized",
      simulationCommtiment: "confirmed",
    });
    
    console.log("-------------------------------");
    console.log("Orders batch:", orderIdsPacks[i]);
    console.log(`Transaction: ${id}`);
    console.log(`View on Solscan: https://solscan.io/tx/${id}`);
    
    // Wait between transactions to avoid rate limits
    await helpers.sleep(5000);
  }

  console.log('Withdrawal complete! ✅');
}

main().catch(console.error);

Understanding the Withdrawal Process

1

Query Unlocked Orders

The script queries the deBridge Stats API for all orders in ClaimedUnlock state where you’re the affiliate fee beneficiary.
2

Check Available Fees

For each order, it checks the order’s escrow wallet to see if there are actual fees to withdraw. Orders without available fees are skipped.
3

Build Instructions

For each order with available fees, a withdrawal instruction is created that will transfer the fees to your associated token account.
4

Optimize Batching

Instructions are packed into transactions that fit within Solana’s packet size limit (1232 bytes), maximizing efficiency.
5

Execute Withdrawals

Each batched transaction is sent and confirmed on-chain, with a 5-second delay between transactions.

Key Components

Withdrawal Instruction

The withdrawal instruction requires these account keys:
const ix = new TransactionInstruction({
  keys: [
    { isSigner: true, isWritable: true, pubkey: beneficiary },
    { isSigner: false, isWritable: true, pubkey: beneficiaryTokenAccount },
    { isSigner: false, isWritable: true, pubkey: orderStateAccount },
    { isSigner: false, isWritable: true, pubkey: orderWalletAddress },
    { isSigner: false, isWritable: false, pubkey: tokenMint },
    { isSigner: false, isWritable: false, pubkey: tokenProgram },
  ],
  programId: dlnProgramId,
  data: Buffer.concat([discriminator, orderId]),
});
The discriminator [143, 79, 158, 208, 125, 51, 86, 85] identifies the withdrawAffiliateFee instruction.

Transaction Batching

Solana transactions are limited to 1232 bytes. The script automatically:
  • Adds compute budget instructions for priority fees
  • Tests transaction size as instructions are added
  • Splits into multiple transactions when size limit is reached
  • Packs as many withdrawals as possible per transaction
On average, you can fit 15-20 withdrawal instructions in a single transaction, significantly reducing your gas costs compared to individual withdrawals.

Environment Setup

Configure your environment variables:
.env
SOL_PRIVATE_KEY=your_base58_encoded_private_key
SOL_RPC_URL=https://api.mainnet-beta.solana.com
Store your private key securely and never commit it to version control. Use environment variables or a secure key management system.

Running the Script

Execute the batch withdrawal:
ts-node src/scripts/affiliates/sol-batch-withdraw.ts
Expected output:
Unclaimed orders: 45
Total instructions: 45, total transactions: 3
Withdrawal started...
-------------------------------
Orders batch: ["0x...", "0x...", ...]
Transaction: 2ZE7s8X...
View on Solscan: https://solscan.io/tx/2ZE7s8X...
-------------------------------
...
Withdrawal complete!

Filtering by Referral Code

You can filter orders by referral code when querying:
const orders = await getUnlockedOrders({
  chainIds: [ChainId.Solana],
  beneficiaryAddress: new PublicKey(beneficiaryPubkey),
  ref: "30830"  // Your referral code
});

Gas Optimization

The script includes compute budget instructions to optimize transaction costs:
const baseInstructions = [
  ComputeBudgetProgram.setComputeUnitLimit({ units: 300_000 }),
  ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 30_000 }),
];
You can adjust these values based on network conditions:
  • Lower priority: Reduce microLamports to 10,000-20,000
  • Higher priority: Increase microLamports to 50,000-100,000

Error Handling

The script includes error handling for common issues:
If no orders are found, verify:
  • Your beneficiary address is correct
  • Orders have reached ClaimedUnlock state
  • You’re checking the correct chain ID
If orders exist but no fees are available:
  • Check that orders have non-zero affiliate fees
  • Verify the order escrow wallets have balances
  • Ensure fees haven’t already been withdrawn
If transactions fail:
  • Ensure you have enough SOL for transaction fees
  • Check RPC rate limits (5s delay helps)
  • Verify your wallet has the required permissions

Source Code Reference

For the complete implementation:
  • Full batch withdraw script: /home/daytona/workspace/source/src/scripts/affiliates/sol-batch-withdraw.ts:1-247

Next Steps

Affiliate Overview

Learn more about how affiliate fees work in deBridge

Create Affiliate Orders

Start creating orders with affiliate fee integration

Build docs developers (and LLMs) love