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
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 | mempool + block validation | MAX_PUBLISH_TEMPLATES_PER_TRANSACTION = 1 |
| No template publish in fee instructions | mempool + block validation | nothing sources a fee by publishing |
| Template binary ceiling | mempool + block validation | ENGINE_LIMITS.max_template_binary_size_bytes (1 MiB) |
| 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, and a floor ofINVOCATION_FLOOR(30) on any instruction that invokes a template — an argument-free call weighs nothing by its arguments, but still instantiates the template. It prices what the network must gossip, store and validate regardless of what execution does.
Stage 1 — the fee intent
The fee instructions exist to source a payment and hand it to the engine: withdrawing from an account, claiming a Minotari burn, swapping through an AMM, revealing a stealth UTXO. Publishing a template is the one thing refused here — the compile it pays for costs two orders of magnitude more than the credit below, and nothing sources a fee by publishing.
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. Loading is what is deduplicated, not calling: every invocation instantiates the template afresh and paysinstantiation_pointsfor it (see below), including nested cross-template calls.- 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
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:StorageandSubstateCreateare computed over the substates the fee intent mutated, plus the transaction receipt it would produce.TemplatePublishcannot arise here, since a fee intent may not publish. - Test the payment. The check is
payments ≥ total charges. The exhaust burn is a share of what is paid rather than a charge, so it does not enter here. 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.
Caution
After the checkpoint, pay_fee is a hard error (FeePaymentInMainIntent). Everything the main
intent spends must already have been paid for.
Stage 3 — the main intent
The main instructions run against the post-checkpoint state and keep accruing RuntimeCall and
TemplateLoad 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( 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
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 |
Note
WASM and native points are divided once, over the transaction total, not per call. Dividing per call would let a transaction split work into sub-divisor chunks and pay zero for each.
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
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 exhaust burn is taken out of what was collected.
exhaust_burn = ⌊total_fees_paid × exhaust_burn_rate / 10_000⌋, overcharge included, and is recorded on the fee receipt. - 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), and the exhaust burn is destroyed and carried in the block header.
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): the whole of what was paid.
The burn is a share of what was collected, not a charge. The fee table alone is the payer’s price; the rate only splits what was paid between leaders and the burn:
| payer pays | leaders receive | burned | |
|---|---|---|---|
charges F, rate s bps | F | F − ⌊F × s / 10_000⌋ | ⌊F × s / 10_000⌋ |
Three consequences follow, and each shows up as a rule elsewhere on this page:
- A payment of
Pcovers charges ofP. The checkpoint test and the compute allowance compare the payment against the charges directly. - The payment a set of charges requires is the charges. That is the figure a rejected payer is told to retry with.
- A dry-run estimate does not depend on the rate, so a wallet prices a transaction from the fee table alone.
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 |
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 |
NativeExecution is where the two largest non-storage charges live, and neither is obvious from the
rate alone:
| Native work | Points | Roughly |
|---|---|---|
| Instantiating a template — every call, including nested cross-template ones | 100,000 + 5×data-segment byte + 700×data segment + 32×element entry + 16×table slot | ~197 µT per call into the builtin account template, ~150 µT into the faucet |
| Compiling a published binary, once at publish | 140,000,000 + 2,100 × binary bytes | ~0.34 tTARI for a 96 KiB template, ~2.3 tTARI at the 1 MiB cap |
The compile charge is what sets the binary cap: MAX_NATIVE_POINTS_PER_TRANSACTION has to hold it
and still leave a transaction room to source its fee. A four-call transaction pays roughly 600–800 µT
of instantiation before any template code runs, which is usually larger than its WasmExecution.
A source that charges nothing is omitted from the breakdown entirely rather than recorded as a zero.
The exhaust burn is not a source: it is settled over the total paid and recorded on the receipt’s
exhaust_burn field. FeeSource::Reserved holds the slot the surcharge occupied, only so receipts
persisted before the share model still decode; nothing writes it.
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 (12 µ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 allowance bounds the pair in both directions. The exhaust burn rate does not enter it.
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.
Tip
required_fees() is a floor, not a recommendation. Overpayment returns to the paying vault, so a
caller paying from an account loses nothing by submitting above it. A caller paying from a bucket
has no refund path and should sit closer to the floor — but not below a figure a build was made
at, where the fee feeds back into the shape.
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
| 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
- Transaction Overview — how fee instructions fit into a transaction
- Stealth Transfers — paying a fee from a stealth UTXO