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 need | Use 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 state | A fixed feeRate you decide based on your own data |
| To send the entire UTXO balance with no leftover | deductFee: true config + estimated feeRate |
| To bid for ordering on Solana under load | priorityFeeMicroLamports > 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 type | vBytes per input | vBytes per output | Base overhead |
|---|---|---|---|
| P2PKH (legacy) | 148 | 34 | 10 |
| P2SH-P2WPKH (nested SegWit) | 91 | 32 | 10.5 |
| P2WPKH (native SegWit) | 68 | 31 | 10.5 |
| P2TR (Taproot) | 57.5 | 43 | 10.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)
| Goal | sat/vByte (mainnet, typical) | Where to look |
|---|---|---|
| Economy (next 24h) | 2–5 | mempool.space → "Low priority" |
| Normal (within 1 hour) | 5–15 | mempool.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:
| Chain | Smallest unit | Block time | Typical mainnet rate |
|---|---|---|---|
| Bitcoin | satoshi | 10 min | 5–50 sat/vByte |
| Litecoin | litoshi | 2.5 min | 1–10 lit/vByte |
| Dogecoin | koinu | 1 min | 1,000+ koinu/vByte (DOGE has a minimum fee policy of ~0.01 DOGE per kB) |
| Dash | duff | 2.5 min | 1–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
| Goal | Micro-lamports per CU | Notes |
|---|---|---|
| Low — non-urgent | 0 (pass null) | Uses default mempool ordering |
| Normal — competitive inclusion | 10,000–50,000 | Beats most untipped traffic |
| Fast — front of the line | 100,000–1,000,000 | For MEV-sensitive flows |
| Extreme — emergency liquidation | 1,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.