Guides

Customizing Transaction Fees

How to pick a custom fee rate for UTXO chains (BTC, LTC, DOGE, DASH) and a priority fee for Solana. Covers VBytes estimation, mempool monitoring, and tradeoffs between cost and confirmation speed.

Customizing Transaction Fees

Every BUILD_*_TRANSACTION component estimates a sensible fee for you. But for production workflows you usually want explicit control — to optimize cost, hit a SLA, or drain a wallet. This guide explains the fee model on each chain and how to compute a custom value.


When to override the default

The default behaviour fetches the network's half-hour confirmation target fee and uses that. Override when:

You needUse a custom…
Lower cost on non-urgent transfers (treasury sweeps, batched payouts)feeRate below the default
Faster inclusion under load (arbitrage, liquidations)feeRate above the default
Predictable cost regardless of mempool stateA fixed feeRate you decide based on your own data
To send the entire UTXO balance with no leftoverdeductFee: true config + estimated feeRate
To bid for ordering on Solana under loadpriorityFeeMicroLamports > 0

UTXO chains: feeRate (sat/vByte, lit/vByte, koinu/vByte, duffs/vByte)

All four UTXO BUILD components — Bitcoin, Litecoin, Dogecoin, Dash — accept an optional feeRate input in the chain's smallest unit per virtual byte (vByte). When you pass null, the component falls back to the live network estimate; when you pass a positive number, that exact rate is used.

import { WorkspaceClient, ComponentModule } from '@zafeguard/caller-sdk';

const workspace = new WorkspaceClient({ apiKey: process.env.ZAFEGUARD_API_KEY! });

await workspace.call(ComponentModule.BUILD_BITCOIN_TRANSACTION, {
  recipientAddress: 'bc1q...',
  amount: 50_000,
  publicKey: '02abc...',
  locktime: null,
  opReturnData: null,
  changeAddress: null,
  feeRate: 25, // 25 sat/vByte — overrides the network estimate
}).promise();

How total fee is computed

totalFee = estimatedVBytes × feeRate

The component computes estimatedVBytes for you based on:

  • Number of inputs selected from your UTXO set
  • Number of outputs (recipient + change + optional OP_RETURN)
  • Payment type (P2PKH / P2SH-P2WPKH / P2WPKH / P2TR — each has different witness sizes)

You only need to choose feeRate. The returned estimatedFee output tells you the total.

Estimating vBytes yourself

If you want to model fee cost up front (e.g. in a dashboard estimate), use these per-payment-type formulas:

Payment typevBytes per inputvBytes per outputBase overhead
P2PKH (legacy)1483410
P2SH-P2WPKH (nested SegWit)913210.5
P2WPKH (native SegWit)683110.5
P2TR (Taproot)57.54310.5
vBytes = (inputs × vBytes_per_input) + (outputs × vBytes_per_output) + base
totalFee = ceil(vBytes) × feeRate

A typical 1-input/2-output P2WPKH transfer: 68 + 2×31 + 10.5 ≈ 141 vBytes. At 20 sat/vByte that's 141 × 20 = 2,820 sats (~$1.50 at $50k BTC).

Picking a rate (Bitcoin)

Goalsat/vByte (mainnet, typical)Where to look
Economy (next 24h)2–5mempool.space → "Low priority"
Normal (within 1 hour)5–15mempool.space "Medium"
Fast (next block)15–50+mempool.space "High priority"

Never go below the network minimum relay fee (1 sat/vByte on Bitcoin). Transactions below that get dropped from mempools.

Pull a live recommendation from https://mempool.space/api/v1/fees/recommended and feed it into your workflow via API_CALL if you want fully automated rate selection.

Litecoin / Dogecoin / Dash

The same formulas apply, but block times and typical fees are very different:

ChainSmallest unitBlock timeTypical mainnet rate
Bitcoinsatoshi10 min5–50 sat/vByte
Litecoinlitoshi2.5 min1–10 lit/vByte
Dogecoinkoinu1 min1,000+ koinu/vByte (DOGE has a minimum fee policy of ~0.01 DOGE per kB)
Dashduff2.5 min1–5 duff/vByte

Dogecoin's minimum fee policy is enforced at the protocol level — fees that are too low cause the tx to be rejected by relay nodes. Stick to the live network estimate (feeRate: null) unless you have a specific reason to override.


Solana: priorityFeeMicroLamports

Solana's fee model is fundamentally different from UTXO chains. Every signature costs a fixed 5,000 lamports (set by the protocol — not by the user). Compute-unit consumption is the variable. Validators order pending transactions by priority fee per compute unit (in micro-lamports per CU).

await workspace.call(ComponentModule.BUILD_SOLANA_TRANSACTION, {
  jsonRpcUrl: 'https://api.mainnet-beta.solana.com',
  feePayer: '...',
  from: null,
  to: '...',
  lamports: '1000000000',
  instructions: [],
  nonceAccount: null,
  priorityFeeMicroLamports: 50_000, // 0.05 lamports per CU
}).promise();

When you pass null, no priority fee is added — the transaction uses the protocol default. When you pass a positive integer, the component prepends a ComputeBudgetProgram.setComputeUnitPrice instruction with your value.

Total Solana fee

totalFee = (signature_count × 5,000)                     ← base, protocol-fixed
         + (compute_units_consumed × priorityFeeMicroLamports / 1_000_000)

A simple SOL transfer consumes ~200 compute units. With priorityFeeMicroLamports: 50_000 that's 200 × 50_000 / 1_000_000 = 10 lamports extra on top of the 5,000 base fee — a 0.2% increase for substantial reordering benefit during congestion.

A token swap or complex DeFi interaction can consume 100,000+ CUs, so the same priority rate has a bigger absolute cost.

Picking a priority fee

GoalMicro-lamports per CUNotes
Low — non-urgent0 (pass null)Uses default mempool ordering
Normal — competitive inclusion10,000–50,000Beats most untipped traffic
Fast — front of the line100,000–1,000,000For MEV-sensitive flows
Extreme — emergency liquidation1,000,000+Cost rises linearly

Pull live recommendations from the RPC's getRecentPrioritizationFees method, or use a service like Helius's getPriorityFeeEstimate, then feed the value into your workflow.


Reading the live estimate

Both UTXO and Solana builds always return estimatedFee in the chain's smallest unit, regardless of whether you passed a custom feeRate or not. This lets workflows surface cost up front:

const { estimatedFee } = await workspace
  .call(ComponentModule.BUILD_BITCOIN_TRANSACTION, { /* … */ })
  .promise();

console.log(`This transaction will cost ${estimatedFee} satoshis`);

Use this output downstream — for example to abort if the fee exceeds a threshold, or to display to an end user before they confirm.


EVM gas

EVM components handle gas slightly differently. BUILD_EVM_TRANSACTION auto-estimates gasLimit from the RPC and applies the network's current fee data with a 20% safety buffer. You can override gasLimit directly via the input, but gasPrice / maxFeePerGas are fetched live from the RPC and not currently overrideable per-call. If you need that level of control, build the transaction manually with BUILD_EVM_CALLDATA + a custom API_CALL to your RPC's eth_sendRawTransaction.

On this page