Fees
Every transaction pays validators for the work it causes: bytes written to permanent state, compute executed, cryptography verified, and the bandwidth its own body consumes. Fees are always paid in TARI, and every amount on this page is in microtari (µT) — 1 TARI = 1,000,000 µT.
A transaction is executed in two halves. The fee intent (the fee_instructions) runs first and
its only job is to source and hand over a payment. The main intent (the instructions) runs
second and does the actual work. Between them sits the fee checkpoint, which is both a gate and a
savepoint: if the main intent later fails, the transaction falls back to the state the fee intent
ended on, and the validator is still paid for the work it did.
The lifecycle of a fee
Section titled “The lifecycle of a fee”Before anything executes
Section titled “Before anything executes”These are checked at ingress or at the very start of execution, before a single fee is metered:
| Rule | Where | Limit |
|---|---|---|
| Transaction weight ceiling | mempool admission | max_transaction_weight (1,000,000) |
| Transaction validity window | mempool + consensus | max_epoch at most max_transaction_validity_epochs (2160) ahead |
| At most one template publish | execution | MAX_PUBLISH_TEMPLATES_PER_TRANSACTION = 1 |
| Fees are payable only in TARI | execution | any other resource is an InvalidArgument |
The first charges land as soon as the runtime initialises, before any instruction runs:
Initial— a flat cost per transaction. Currently0on every shipped network.TransactionWeight— the transaction’s weight ×per_transaction_weight_cost. Weight is a static size/IO measure: instruction literals (bytes ÷ 3), blobs, 15 per input and 5 per signer. It prices what the network must gossip, store and validate regardless of what execution does.
Stage 1 — the fee intent
Section titled “Stage 1 — the fee intent”The fee instructions exist to source a payment and hand it to the engine. Anything is allowed here: withdrawing from an account, claiming a Minotari burn, swapping through an AMM, revealing a stealth UTXO.
How a payment is made
| Builder call | Under the hood | Refundable? |
|---|---|---|
.pay_fee_from_component(account, max_fee) |
calls pay_fee on the component, which pays from its TARI vault |
Yes — surplus returns to that vault |
.pay_fee_from_bucket("bucket") |
consumes a workspace bucket outright | No — there is no vault to refund to |
Rules and checks applied to a payment:
- The resource must be
TARI; anything else is rejected. - The amount must be greater than zero and must not overflow
u64when added to the running total. - The paying vault must not be frozen for withdrawals, and the resource’s
Withdrawaccess rule must pass. - A stealth-funded fee intent may perform at most one stealth transfer
(
STEALTH_LIMITS.max_fee_intent_transfers), counted whether it comes from aStealthTransferinstruction or from a template callingResourceManager::stealth_transfer.
Charges that accrue while the fee intent runs
RuntimeCall— a flatper_module_call_costfor every engine host call, plusper_byte_storage_cost × message_bytes / log_bytes_cost_divisorfor a log’s message. A log is retained in each validator’s record of the execution rather than in consensus state, so its bytes are priced well below persisted ones.TemplateLoad—(bytes_loaded / 3000) × per_template_load_cost_unit, charged once per template per transaction; repeat calls into an already-loaded template are free.SignatureVerification—per_signature_verification_costper signature verified.- Metering points accumulate for WASM and native crypto, but are not priced until finalisation.
Compute on credit. A transaction cannot pay before it has sourced its funds, so the fee intent
runs on a credit of FREE_COMPUTE_GRACE_POINTS (32,000,000 points) — roughly 3× the most expensive
legitimate fee-sourcing flow. Exhaust it and execution traps out of gas, reported as
FeeIntentComputeExceeded. It is the hard bound on free compute a transaction can extract from a
validator.
The credit is flat: paying does not raise it. A fee intent that fails leaves no checkpoint to fall back to, so the transaction settles as a rejection that collects nothing — compute funded by a payment here would be work done and never paid for, repeatably, with the same funds. That would also make the main intent pointless: identical work, free when it fails. Anything needing more than the credit belongs in the main instructions, where the fee already paid funds it and a failure still commits the fee intent.
Stage 2 — the fee checkpoint
Section titled “Stage 2 — the fee checkpoint”The checkpoint is where “can this transaction afford to exist?” is answered. It runs after the last fee instruction and before the first main instruction, and it does four things in order:
- Validate the intent is finalisable. Dangling buckets or undropped proofs fail here, exactly as they would at the end of a transaction.
- Price the fee intent’s state (
on_fee_checkpoint). This is the state a failing transaction falls back to committing, so it is priced before the main intent spends any compute:Storage,SubstateCreateandTemplatePublishare computed over the substates the fee intent mutated, plus the transaction receipt it would produce. - Test the payment. The check is
spendable(payments) ≥ total charges, notpayments ≥ charges— the exhaust burn is taken on top of the charges, so a payment that exactly matches them cannot also cover the exhaust burn. A shortfall rejects the transaction outright: nothing is written and no fee is taken. - Snapshot and reset. The working state is cloned as the fallback; the workspace is cleared and its proofs dropped, so the main intent starts with a clean workspace.
Stage 3 — the main intent
Section titled “Stage 3 — the main intent”The main instructions run against the post-checkpoint state and keep accruing RuntimeCall,
TemplateLoad and SignatureVerification charges as before. What changes is the compute budget.
Compute is funded by what the payment has left. The allowance is:
allowance = points_funded_by( spendable(payments) − charges_so_far )The flat credit is gone — sourcing the fee was its only purpose, and that is done. Funding compute from the unspent part of the payment (rather than from the whole of it) is what keeps a transaction from spending its entire payment on execution and then being unable to afford even its own fee-intent fallback, which would settle as a rejection that collects nothing.
Three ceilings apply on top of each other:
| Bound | Value | Effect when hit |
|---|---|---|
| Payment-funded allowance | derived, see above | out-of-gas reported as InsufficientFeesForCompute |
| Fee-intent credit (stage 1 only) | 32,000,000 points, flat | out-of-gas reported as FeeIntentComputeExceeded |
MAX_WASM_POINTS_PER_TRANSACTION |
250,000,000 | out-of-gas, hard cap |
MAX_NATIVE_POINTS_PER_TRANSACTION |
2,400,000,000 | MaxNativeExecutionPointsExceeded |
Native verification (stealth transfers, confidential withdrawals, Minotari burn claims) is pre-charged against the same allowance before the cryptography runs, so a transaction that cannot afford it traps without the validator having done the work. WASM is metered by Wasmer: each invocation’s meter is capped to the smaller of the remaining hard cap and the remaining paid allowance, so work cannot be split across instructions or nested calls to get a fresh budget each time.
If an instruction fails, execution rolls back to the fee checkpoint. The fee state carries across the rollback — every charge metered before the failure is still owed.
Stage 4 — finalisation
Section titled “Stage 4 — finalisation”Finalisation is a two-pass charge, because the receipt’s cost depends on which state is persisted, and which state is persisted depends on the cost.
Pass 1 — on_before_finalize. Price the state execution actually ended on. This is what the
commit-or-reject decision is made against, and it is the figure reported as total_fees_required — the
number a resubmission has to clear.
Selection. If the main intent failed, or the payment does not cover the charges from pass 1, the
state to persist becomes the fee checkpoint instead of the working state — an
AcceptFeeRejectRest outcome.
Pass 2 — on_before_persist. Re-price the state that was actually chosen; the second result
supersedes pass 1’s. This pass is skipped on a full commit, where the two states are the same.
The charges computed in these passes:
| Source | Formula |
|---|---|
Storage |
(bytes of persisted substates + receipt bytes) × per_byte_storage_cost / storage_cost_divisor |
SubstateCreate |
(newly created substates + 1 for the receipt) × per_substate_create_cost |
TemplatePublish |
flat per_template_publish_cost + first template_size_premium_free_bytes at the storage rate + units² × per_template_size_premium_unit_cost |
WasmExecution |
(total points / wasm_points_cost_divisor) × per_wasm_point_cost |
NativeExecution |
same rate, over the accumulated native points |
ExhaustBurn |
total of all charges above × exhaust_burn_rate / 10_000 |
The final check. After pass 2, payments ≥ charges is tested one last time. A fee-intent commit
still persists real state — substates and a receipt carrying every event the intent emitted — so a
payment that cannot cover that commits nothing at all. The transaction is rejected, nothing is
taken, and the receipt reports the charges as metered against no payment so the payer knows what to
retry with. Only a fee-intent commit can fail here — a full commit’s charges were tested against the
payment to reach this point.
Settlement
Section titled “Settlement”Once the charges are final:
- Non-refundable payments are collected in full — bucket payments have no return vault, so the
whole amount is taken even if it exceeds the charges. The excess is recorded as
total_fee_overcharge. - Refundable payments are drawn down only to the amount still owed.
- Whatever is left of a refundable payment goes back to its vault.
- The overcharge is split as though it were a payment inclusive of its own exhaust burn: the
burn share moves into
ExhaustBurn, the rest is kept by the network. - The transaction receipt is written into the substate diff, carrying the fee receipt, the events, the diff summary and the outcome.
Fees flow onward as two separate quantities: leaders are paid pre_burn_fees_paid (the total paid
minus the exhaust burn) in full, and the exhaust burn is destroyed and carried in the block
header.
The exhaust burn
Section titled “The exhaust burn”exhaust_burn_rate is a consensus constant — 500 bps (5%) on mainnet — and every validator must
use the same value or nodes diverge on block headers. It is an ExhaustBurnRate, which cannot hold a
rate above MAX_EXHAUST_BURN_RATE_BPS (10 000 bps): that is the rate the dry-run allowance is
derived against, so a network burning faster would under-state what every estimate reports.
The exhaust burn is charged on top of the accrued fee, not deducted from it. Two consequences follow, and both show up as rules elsewhere on this page:
- A payment of
Pcan only cover charges ofP / (1 + rate). That is thespendable()function used at the checkpoint and by the compute allowance. - The payment a set of charges requires is
ceil(charges × (1 + rate)), rounded up. That is the figure a rejected payer is told to retry with.
What a transaction is charged for
Section titled “What a transaction is charged for”Rates below are the current testnet values (mainnet placeholders are identical and still to be finalised).
| Source | When | Testnet rate |
|---|---|---|
Initial |
runtime init | 0 |
TransactionWeight |
runtime init | 1 µT per weight unit |
RuntimeCall |
every engine host call | 1 µT, plus 1 µT per 64 bytes of a log message |
TemplateLoad |
first load of each template | 10 µT per 3 KB |
SignatureVerification |
per signature | 10 µT |
Storage |
finalisation | 1 µT per byte |
SubstateCreate |
finalisation | 25 µT per new substate |
TemplatePublish |
finalisation | 250,000 µT flat; first 96 KiB at the storage rate; 100 µT × units² per KiB beyond |
WasmExecution |
finalisation | 1 µT per 1000 metering points |
NativeExecution |
finalisation | 1 µT per 1000 point-equivalents |
ExhaustBurn |
finalisation | 5% of the total |
A source that charges nothing is omitted from the breakdown entirely rather than recorded as a zero.
Estimating what to submit
Section titled “Estimating what to submit”Submit a transaction as a dry run and the engine meters it exactly as it would for real, but never
aborts for insufficient payment. Read FinalizeResult::required_fees() from the result and use it as
the max_fee.
That figure is total_fees_required + FEE_ESTIMATE_ALLOWANCE (25 µT). The allowance exists because
max_fee is itself an input to the cost, so the real run meters slightly differently from the dry
run that estimated it:
- Transaction weight prices the fee instruction’s literals by their encoded bytes, so a wider
max_feecosts marginally more weight. - The storage tally byte-counts the fee vault before the unspent payment is returned, so a wider
max_feeleaves a narrower residual there and costs marginally less. - The exhaust burn re-multiplies both.
The allowance bounds the pair in both directions at any exhaust burn rate up to
MAX_EXHAUST_BURN_RATE_BPS.
An estimate describes the transaction it was metered on, which matters wherever the fee feeds back
into the transaction’s shape. Stealth input selection targets amount + max_fee, so the fee decides
which UTXOs are spent and whether any change is left over — and a change output is another stealth
output to verify. A figure taken at a guessed fee therefore prices a shape the real submission need
not have, so it has to be re-derived until a build at it pays for itself. The wallet daemon’s
accounts.stealth_transfer does that internally for its dry runs; accounts.confidential_transfer
answers from a single round and carries the same caveat.
Where the shape allows, that settling is arithmetic rather than round trips.
MergedStealthTransferShape::estimate_fee prices the shape of an ordinary send — one stealth
transfer statement that both moves the funds and reveals the fee — from its input and output counts,
its outputs’ stored size and its transaction weight. The wallet daemon rebuilds locally until the
estimate no longer exceeds the fee the build was made at, then spends one dry run confirming it. A
shape carrying anything the estimate does not price (a badge proof, a withdraw of revealed funds, a
recipient owed revealed funds, a fee sourced by a statement of its own) settles over the wire as
before.
Selection is what forces the figure that gets reported. It is branch-and-bound over the total value selected, not the input count, so a target one microtari lower can pick a different and larger set — a different transaction at a different price. A fee is therefore only safe to report if some build was actually made at it, which is why a settled estimate answers with the fee its confirming run was built at rather than the lower figure that run turned out to need. The difference is the estimate’s own margin, single-digit microtari.
The one bound a dry run does enforce is the fee intent’s compute credit. It is a flat figure rather
than one derived from a payment, so it is the same for an estimate as for a real run — a fee intent
above it fails estimation with FeeIntentComputeExceeded instead of first surfacing at submission,
where the only remedy is to restructure the transaction.
Note that FeeReceipt::required_fees() reads only what that receipt was charged — on an
AcceptFeeRejectRest outcome that is the fee intent’s own cost, not what the transaction needed. Use
FinalizeResult::required_fees() when telling a caller what to resubmit with.
Outcomes
Section titled “Outcomes”| Outcome | State persisted | Fee taken |
|---|---|---|
Accept |
the full transaction | charges in full, surplus refunded |
AcceptFeeRejectRest |
the fee checkpoint only | charges in full, surplus refunded |
Reject at the fee checkpoint |
none | none |
Reject at finalisation |
none | none; the receipt reports what a commit would have cost |
In both accepting cases, FinalizeResult::total_fees_required tells you what committing the whole
transaction was priced at, which is the number a retry has to clear — on an AcceptFeeRejectRest that
is higher than the receipt’s own total.
See also
Section titled “See also”- Transaction Overview — how fee instructions fit into a transaction
- Stealth Transfers — paying a fee from a stealth UTXO