Skip to content

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.


How a transaction is charged, top to bottomTransaction submittedfee instructions · main instructions · inputs · max_feecharge: Initial + TransactionWeight1 · Fee intentThe fee instructions run: withdraw, claim a Minotari burn, swap, reveal.Charges accrue per host call, template load and signature check.Compute allowance = a flat 32M credit, whatever has been paid.pay_fee records a payment — TARI only, and never zero.2 · Fee checkpointon_fee_checkpoint prices the state the fee intent would persist.CHECK spendable(payments) ≥ charges so far?The state is snapshotted; the workspace is cleared and proofs dropped.This snapshot is the fallback the transaction can still commit.RejectedInsufficientFeesPaid.Nothing written, nothing taken.3 · Main intentThe main instructions run against the checkpointed state.Compute allowance = the part of the payment the charges have not spent.The flat credit is gone; a further pay_fee is rejected here.Charges keep accruing per host call and per verification.Instruction failureFall back to the checkpoint;charges so far are kept.4 · FinalisationPass 1 — on_before_finalize prices the state execution ended on.Select what to persist: the whole transaction, or the fee intent alone.Pass 2 — on_before_persist re-prices whichever state was chosen.Storage · SubstateCreate · TemplatePublish · Wasm/Native · ExhaustBurnCHECK payments ≥ the recomputed charges?RejectedNothing persisted or taken;the receipt carries the retry fee.SettlementCharges collected; the unspent payment is refunded to its vault.Unrefundable overpayment is kept and split with the exhaust burn.The exhaust burn is destroyed; leaders are paid the pre-burn fee.The transaction receipt is up’d into the substate diff.Accept — full commitor AcceptFeeRejectRest — the fee intent alone commits

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. Currently 0 on 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.

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 u64 when added to the running total.
  • The paying vault must not be frozen for withdrawals, and the resource’s Withdraw access 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 a StealthTransfer instruction or from a template calling ResourceManager::stealth_transfer.

Charges that accrue while the fee intent runs

  • RuntimeCall — a flat per_module_call_cost for every engine host call, plus per_byte_storage_cost × message_bytes / log_bytes_cost_divisor for 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.
  • SignatureVerificationper_signature_verification_cost per 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.


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:

  1. Validate the intent is finalisable. Dangling buckets or undropped proofs fail here, exactly as they would at the end of a transaction.
  2. 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, SubstateCreate and TemplatePublish are computed over the substates the fee intent mutated, plus the transaction receipt it would produce.
  3. Test the payment. The check is spendable(payments) ≥ total charges, not payments ≥ 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.
  4. 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.

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.


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.


Once the charges are final:

  1. 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.
  2. Refundable payments are drawn down only to the amount still owed.
  3. Whatever is left of a refundable payment goes back to its vault.
  4. 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.
  5. 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.


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 P can only cover charges of P / (1 + rate). That is the spendable() 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.

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.


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_fee costs marginally more weight.
  • The storage tally byte-counts the fee vault before the unspent payment is returned, so a wider max_fee leaves 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.


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.