Minting and burning tokens on L2
The reference EUTXO L2 ledger lets a head mint and burn transient tokens — tokens that live
only inside the head and can never reach L1. They ride on top of an ordinary L2 transaction: a normal
Cardano mint field plus a minting policy, and a metadata declaration that marks which minted tokens
are transient.
Read L2TXS.md first — a minting tx is a basic L2 tx with extra metadata. A complete
worked example lives in examples/tutorials/transient-tokens.md
(driven by examples/src/test/scala/hydrozoa/examples/transient/TransientTokenDemo.scala).
The model: two compartments
The L2 ledger keeps each UTxO’s value in two compartments:
- the main compartment — the L1-valid value (ADA and L1-native tokens), exactly what a head closure would pay out; and
- the transient compartment — an overlay of L2-minted tokens keyed by UTxO. ADA is never transient.
Cardano ledger rules and your scripts only ever see the combined view (main + transient). The
l2TransientTokens metadata declaration is the only thing that tells the two apart — nothing keys
off the policy id, so the same policy id can name an L1-native token in main and a transient token in
the overlay at once.
What you can mint or burn
Only transient tokens. The rules, enforced by value conservation (no policy-id check anywhere):
- Mint (
mintpositive) or burn (mintnegative) tokens that you declare as transient. - You cannot mint or burn L1-native (main-compartment) tokens — the arithmetic makes it impossible.
- Per transaction:
overlay_in + mint = total declared transient. (This balances for free because L2 forcesfee = 0and forbids reward withdrawals and certificates, so the combined-view and main-projection conservation checks together pin the transient delta.) - Each declared bundle must be ≤ the assets actually on that output.
- A withdrawal output (its index listed in
l1BoundOutputs; see L2TXS.md) must not declare transient tokens — transient tokens cannot leave the head.
Metadata: declaring transient tokens
A minting tx uses the same 4937 L2 metadata as any L2 tx (see L2TXS.md), adding the
optional l2TransientTokens field alongside l1BoundOutputs and the headId pin:
4937 → {
"L2": {
<headId hex>: {
"l1BoundOutputs": List(Int, …), // indices of withdrawal outputs (empty if none)
"l2TransientTokens": <declarations>, // which minted tokens are transient, per output
}
}
}
The l2TransientTokens declarations map an output index to the transient bundle it carries:
l2TransientTokens = Map(
Int(outputIndex) → Map(
Bytes(policyId /* 28 bytes */) → Map(
Bytes(assetName /* ≤ 32 bytes */) → Int(quantity /* 1 … Long.MaxValue (i64) */)
)
)
)
An index with no declaration carries no transient tokens; a declared index with an empty bundle is malformed. The overlay is keyed by the new UTxO id, so if you move transient tokens to another L2 output in a later tx, you must re-declare them there.
Value conservation (why a mint is accepted)
The ledger runs conservation twice:
- against the combined view — the full Cardano check including the
mintfield, scripts, and signatures; and - against the main projection — the tx rebuilt with
mint = Noneand every output reduced by its declared transient bundle. This second run is what forbids minting/burning L1-native tokens or smuggling overlay tokens into the main compartment.
So: keep ADA and L1-native value conserved in the main projection, and let the mint field plus the
l2TransientTokens declarations account for exactly the transient delta.
Preparing a minting tx
- Build a basic L2 tx (spend an L2 UTxO, add outputs,
fee = 0) — but spend/quote values in the combined view (main + any transient the input already holds). ⚠️ The L2 utxo query (GET /l2/cardano-eutxo/utxos/{address}) returns main-compartment value only — the transient tokens overlaid on a utxo are not shown. You must track a utxo’s transient content yourself (from what you minted or last declared onto it) and add it in, or the combined view won’t balance and the tx is rejected. - Add a Cardano
mintstep under your minting policy (native or PlutusV3): positive to mint, negative to burn. - Put the minted (or remaining) transient tokens on an L2-bound output (its index left out of
l1BoundOutputs), and declare them inl2TransientTokensfor that output index. - Attach the
4937L2metadata (l1BoundOutputs+l2TransientTokens, under the headId pin). - Sign (the policy may require specific signatures) and submit as in L2TXS.md.
To burn, spend the UTxO holding the transient tokens, set mint negative for the burned amount,
and declare only what remains (or l2TransientTokens = {} if none remains).
Caveat: closing a head with live transient tokens
There is currently no finalization gate that blocks closing a head while transient tokens are outstanding. If a head closes with a non-empty transient compartment, those tokens simply cease to exist — holders receive only the backing ADA of the UTxO, not the transient tokens. Burn transient tokens back before closing if their disappearance would matter.