diff --git a/docs/guides/go/index.md b/docs/guides/go/index.md
index a10941f..e00936a 100644
--- a/docs/guides/go/index.md
+++ b/docs/guides/go/index.md
@@ -19,3 +19,4 @@ The wallet-cli versions of the same workflows live in the [command-line guides](
| [Atomic swap](atomic-swap.md) | The Mintlayer side of a BTC ⇄ ML swap with HTLCs |
| [Wallet & signing (WASM)](wallet-wasm.md) | Generate wallets, derive addresses, sign without a daemon |
| [Forging custom transactions](custom-transactions.md) | Mixed inputs/outputs, protocol fees, multi-party signing, intents |
+| [Composing UTXOs](utxo-composition.md) | Update an order atomically: conclude input + create output |
diff --git a/docs/guides/go/staking.md b/docs/guides/go/staking.md
index b1eb54f..f12d544 100644
--- a/docs/guides/go/staking.md
+++ b/docs/guides/go/staking.md
@@ -119,4 +119,4 @@ To estimate rewards: pool block stats for a time range, the pool's `cost_per_blo
## Manual transaction building
-The `wasm` package encodes delegation transactions without the wallet daemon: `EncodeOutputCreateDelegation`, `EncodeOutputDelegateStaking`, `EncodeInputForWithdrawFromDelegation` (nonce from the indexer's `NextNonce`). See [Go SDK: Staking](../../build/sdks/go/staking.md) for the full low-level flow.
+The `wasm` package encodes delegation transactions without the wallet daemon: `EncodeOutputCreateDelegation`, `EncodeOutputDelegateStaking`, `EncodeInputForWithdrawFromDelegation` (nonce from the indexer's `NextNonce`). See [Go SDK: Staking](../../build/sdks/go/staking.md) for the full low-level flow, and [Composing UTXOs](utxo-composition.md) for the spend-and-recreate composition pattern.
diff --git a/docs/guides/go/trading-with-orders.md b/docs/guides/go/trading-with-orders.md
index ab815c3..5f1a7b5 100644
--- a/docs/guides/go/trading-with-orders.md
+++ b/docs/guides/go/trading-with-orders.md
@@ -130,3 +130,5 @@ freezeInput, err := c.EncodeInputForFreezeOrder(orderID, currentBlockHeight, min
```
Concluding returns the unclaimed `give` remainder plus any accumulated `ask` balance to the order's conclude destination. Only the maker can freeze or conclude.
+
+To **update** an existing order (new price or amounts), compose a transaction that consumes the conclude-order input and re-creates the order: see [Composing UTXOs](utxo-composition.md).
diff --git a/docs/guides/go/utxo-composition.md b/docs/guides/go/utxo-composition.md
new file mode 100644
index 0000000..760b7aa
--- /dev/null
+++ b/docs/guides/go/utxo-composition.md
@@ -0,0 +1,171 @@
+---
+title: "Composing UTXOs: Updating an Order"
+description: "How to compose UTXOs on Mintlayer: update a DEX order atomically by consuming its conclude-order input and re-creating it with a create-order output."
+sidebar_position: 9
+---
+
+# Composing UTXOs: Updating an Order
+
+Mintlayer is a UTXO chain: **there is no mutable state**. A DEX order is an unspent output, and every protocol object behaves the same way, so "updating" an order means spending it and re-creating it *in the same transaction*. This atomic spend-and-recreate pattern is the essence of UTXO composition.
+
+## The pattern
+
+An order lives as a `CreateOrder` output holding the `give` balance plus any accumulated `ask` balance. Its spend authority is the maker, gated by the order's **nonce**. To update the order (new price, new amounts), build one transaction that:
+
+1. consumes the order via a **conclude-order input** (this releases the whole order balance), and
+2. re-locks the funds into a fresh **create-order output** with the new parameters.
+
+```mermaid
+flowchart LR
+ OLD["Order UTXO
(give 100, ask 0)"] -->|"conclude-order input
(nonce n)"| TX["Transaction"]
+ FEE["Coin UTXO
(fee payer)"] --> TX
+ TX -->|"create-order output
(give 100, ask 20)"| NEW["New order UTXO
(new order id)"]
+ TX -->|"change"| MAKER["Maker address"]
+```
+
+Two consequences worth internalizing:
+
+- The **new order gets a new ID** (IDs are derived from inputs, see [predicting IDs](custom-transactions.md#predicting-ids)). Publish the new ID and retire the old one.
+- If the transaction fails or is dropped, nothing changed: the old order is still live and its nonce unspent.
+
+:::note[What consensus allows and forbids]
+
+The pattern above is exactly what consensus permits: **one order operation input** (fill, conclude, or freeze) — a slot shared with token account commands, so you cannot combine an order operation with a mint, an unmint, or any other command input — and **one `CreateOrder` output** per transaction. Concluding and re-creating in the same transaction sits precisely at both limits, which is what makes the atomic update work.
+
+Everything past those limits is rejected by the verifier:
+
+- A second command input (order operation or token account command) — `MultipleAccountCommands`.
+- A second `CreateOrder` output — `MultipleOrdersCreated`.
+- Spending a `CreateOrder` output as a UTXO input — orders are not UTXOs; they are consumed only through order operation inputs (`InvalidInputTypeInTx`).
+- Creating an order in a transaction with no UTXO input — the new order's id is derived from the first UTXO outpoint (`NoUtxoInputsForOrderIdCreation`), which is why the fee input below is not optional.
+
+:::
+
+## The ingredients
+
+| Piece | Encoder | Source of truth |
+| ----- | ------- | --------------- |
+| Conclude input | `EncodeInputForConcludeOrder(orderID, nonce, currentBlockHeight, network)` | `Nonce` from the indexer's order record |
+| New order output | `EncodeCreateOrderOutput(askAmount, askTokenID, giveAmount, giveTokenID, concludeAddress, network)` | Your new price/amounts |
+| Surplus / change outputs | `EncodeOutputTransfer` / `EncodeOutputTokenTransfer` | Old balance minus new give amount |
+| Fee input | `EncodeInputForUtxo` | A coin UTXO you own |
+| Witness for the conclude input | `EncodeWitness` with `TxAdditionalInfo` | Order balances from the indexer |
+| Witness for the coin input | `EncodeWitness` | n/a |
+
+## Fetch the order state
+
+```go
+import "github.com/mintlayer/go-sdk/indexer"
+
+idxClient := indexer.New("http://127.0.0.1:3000")
+
+order, err := idxClient.GetOrder(ctx, oldOrderID)
+if err != nil {
+ log.Fatal(err)
+}
+fmt.Printf("give: %s ask: %s nonce: %d\n",
+ order.GiveBalance.Atoms(), order.AskBalance.Atoms(), order.Nonce)
+```
+
+The `AskBalance` matters: any asks accumulated from takers are part of the order you are concluding, so the new order (or a change output) must account for them.
+
+## Build the transaction
+
+Example: top up the `give` side and raise the ask. Old order: give 100 tokens / ask 10 coins. New order: give 150 / ask 12. The conclude releases 100 tokens + accumulated asks; a separate token UTXO adds the extra 50.
+
+```go
+// 1. Consume the old order
+concludeInput, err := c.EncodeInputForConcludeOrder(
+ oldOrderID, order.Nonce, tip.BlockHeight, mintlayer.Mainnet)
+
+// 2. Fee payer
+srcID, err := c.EncodeOutpointSourceId(feeTxIDBytes, mintlayer.SourceTransaction)
+feeInput, err := c.EncodeInputForUtxo(srcID, 0)
+
+// 3. Re-create the order with updated parameters
+var giveToken, askToken *string
+newOrderOutput, err := c.EncodeCreateOrderOutput(
+ mintlayer.NewAmount("12"), askToken, // ask 12 coins (nil token id = coin)
+ mintlayer.NewAmount("150"), giveToken, // give 150 tokens
+ concludeAddress, // same conclude destination
+ mintlayer.Mainnet,
+)
+
+// 4. Any balance not re-locked returns via change outputs
+changeOutput, err := c.EncodeOutputTransfer(
+ mintlayer.NewAmount("2000000000"), makerAddr, mintlayer.Mainnet) // fee change
+
+tx, err := c.EncodeTransaction(
+ append(concludeInput, feeInput...),
+ append(newOrderOutput, changeOutput...),
+ 0,
+)
+```
+
+Token inputs (e.g. a UTXO holding the additional 50 tokens) encode with `EncodeInputForUtxo` like any other spend; only the *outputs* distinguish coin from token value.
+
+## Sign it
+
+The conclude input's sighash requires the order's balances via `TxAdditionalInfo`: this is the part that differs from a plain transfer. Signing is also where the inputs' backing UTXOs come in: `EncodeWitness` takes one entry per input, `0x00` for non-UTXO inputs (the conclude input) and `0x01` plus the encoded output for UTXO inputs (the fee input):
+
+```go
+feeOutput, err := c.EncodeOutputTransfer(
+ feeAmount, makerAddr, mintlayer.Mainnet, // re-encode the fee UTXO's Transfer output
+)
+allUtxoBytes := append([]byte{0x00, 0x01}, feeOutput...)
+
+additionalInfo := mintlayer.TxAdditionalInfo{
+ OrderInfo: map[string]mintlayer.OrderInfo{
+ oldOrderID: {
+ InitiallyAsked: order.InitiallyAsked,
+ InitiallyGiven: order.InitiallyGiven,
+ AskBalance: order.AskBalance,
+ GiveBalance: order.GiveBalance,
+ },
+ },
+}
+
+var witnessBytes []byte
+for i := 0; i < 2; i++ { // one witness per input, in input order
+ w, err := c.EncodeWitness(
+ mintlayer.SigHashAll,
+ makerKey,
+ makerAddr,
+ tx,
+ allUtxoBytes,
+ uint32(i),
+ additionalInfo, // required for the order input
+ tip.BlockHeight,
+ mintlayer.Mainnet,
+ )
+ if err != nil {
+ log.Fatal(err)
+ }
+ witnessBytes = append(witnessBytes, w...)
+}
+
+signedTx, err := c.EncodeSignedTransaction(tx, witnessBytes)
+```
+
+Submit via the indexer or node as usual, then predict and store the new order ID:
+
+```go
+newOrderID, err := c.GetOrderId(append(concludeInput, feeInput...), mintlayer.Mainnet)
+```
+
+## Variations of the same pattern
+
+The conclude-then-recreate transaction is one instance of a general tool. With the same encoders you can:
+
+- **Cancel an order**: conclude input, no create-order output; the balance flows to change outputs.
+- **Self-fill and requote**: conclude input + `FillOrder`-style split: partially fill your own order and re-create a smaller one. (A pure fill by a taker uses `EncodeInputForFillOrder` and needs **no signature** for that input, `EncodeWitnessNoSignature`.)
+- **Freeze before restructuring**: `EncodeInputForFreezeOrder` stops takers while you prepare the replacement transaction.
+- **Chain protocol objects**: the same spend-and-recreate logic composes delegations (withdraw input + `EncodeOutputDelegateStaking` output) and any future output type.
+
+:::note
+
+Exactly which balances must be re-assigned to which outputs is enforced by consensus, not by the encoders, the runtime tells you *if* a transaction is invalid, the indexer tells you *what* the balances are, and the composition is yours to design.
+
+:::
+
+For the order model itself (give/ask, conclude key, freeze semantics), see [Trading with Orders](trading-with-orders.md) and the wallet-cli guide [Trading with orders](../cli/trading-with-orders.md).
diff --git a/docs/guides/index.md b/docs/guides/index.md
index ec78c86..ef3886f 100644
--- a/docs/guides/index.md
+++ b/docs/guides/index.md
@@ -30,4 +30,4 @@ New languages can be added alongside these; the workflows are the same, only the
| Manage a multisig address | [guide](cli/managing-a-multisig-address.md) | CLI only | CLI only | CLI only | CLI only |
| Migrate ERC-20 ML to mainnet | [guide](cli/migrate-erc20-token-to-mainnet.md) | Portal only | Portal only | Portal only | Portal only |
-Deeper full-custody dives with the embedded WASM runtime (Go and Python, no wallet daemon): [Wallet & signing](go/wallet-wasm.md) and [Forging custom transactions](go/custom-transactions.md), each also available in the [Python guides](python/index.md). The Rust equivalent is the native `crypto` module, covered by [Cryptography](../build/sdks/rust/crypto.md) and [Building transactions](../build/sdks/rust/transactions.md).
+Deeper full-custody dives with the embedded WASM runtime (Go and Python, no wallet daemon): [Wallet & signing](go/wallet-wasm.md), [Forging custom transactions](go/custom-transactions.md), and [Composing UTXOs](go/utxo-composition.md), each also available in the [Python guides](python/index.md). Composing UTXOs also exists in [JavaScript](javascript/utxo-composition.md) (raw `wasm-lib` encoders) and [Rust](rust/utxo-composition.md) (native `crypto` module, covered by [Cryptography](../build/sdks/rust/crypto.md) and [Building transactions](../build/sdks/rust/transactions.md)).
diff --git a/docs/guides/javascript/index.md b/docs/guides/javascript/index.md
index 040e946..44f2758 100644
--- a/docs/guides/javascript/index.md
+++ b/docs/guides/javascript/index.md
@@ -16,4 +16,5 @@ The wallet-cli versions of the same workflows live in the [command-line guides](
| [Issue an NFT](issue-nft.md) | MLS-03 issuance and transfers |
| [Staking and delegation](staking.md) | Create a delegation, stake, withdraw |
| [Trading with orders](trading-with-orders.md) | On-chain orders: create, discover, fill, conclude |
+| [Composing UTXOs](utxo-composition.md) | Update an order atomically: conclude input + create output |
| [Atomic swap](atomic-swap.md) | The Mintlayer side of a BTC ⇄ ML swap with HTLCs |
diff --git a/docs/guides/javascript/trading-with-orders.md b/docs/guides/javascript/trading-with-orders.md
index 3a88d96..5252893 100644
--- a/docs/guides/javascript/trading-with-orders.md
+++ b/docs/guides/javascript/trading-with-orders.md
@@ -59,6 +59,8 @@ The order creator can close an order and reclaim the remaining balance:
const signedTx = await client.concludeOrder('ord1...');
```
+To **update** an existing order (new price or amounts), compose a transaction that consumes the conclude-order input and re-creates the order: see [Composing UTXOs](utxo-composition.md).
+
## Example: market-maker loop
```ts
diff --git a/docs/guides/javascript/utxo-composition.md b/docs/guides/javascript/utxo-composition.md
new file mode 100644
index 0000000..bc1cc6d
--- /dev/null
+++ b/docs/guides/javascript/utxo-composition.md
@@ -0,0 +1,208 @@
+---
+title: "Composing UTXOs: Updating an Order"
+description: "How to compose UTXOs in JavaScript: update a DEX order atomically by consuming its conclude-order input and re-creating it with a create-order output."
+sidebar_position: 9
+---
+
+# Composing UTXOs: Updating an Order
+
+Mintlayer is a UTXO chain: **there is no mutable state**. A DEX order is an unspent output, and every protocol object behaves the same way, so "updating" an order means spending it and re-creating it *in the same transaction*. This atomic spend-and-recreate pattern is the essence of UTXO composition.
+
+This guide uses the low-level encoders from `@mintlayer/wasm-lib` — the same WebAssembly module the [JavaScript SDK](../../build/sdks/javascript/index.md) is built on — plus `MintlayerApiProvider` for reading chain state. The same workflow in Go and Python lives in the [Go](../go/utxo-composition.md) and [Python](../python/utxo-composition.md) guides.
+
+## The pattern
+
+A maker wants to update an existing order's conditions: lower the `ask` from 10 coins to 12 coins, raise the `give` from 100 tokens to 150 tokens. The transaction must contain:
+
+```mermaid
+flowchart LR
+ C["Conclude order input
(consumes the old order)"] --> N["Transaction"]
+ F["Fee input
(UTXO paying the fee)"] --> N
+ N -->|"create-order output
(give 150, ask 12)"| O["New order"]
+ N -->|"change"| R["Maker address"]
+```
+
+All four effects happen atomically. If the transaction fails, the old order is untouched.
+
+:::note[What consensus allows and forbids]
+
+The pattern above is exactly what consensus permits: **one order operation input** (fill, conclude, or freeze) — a slot shared with token account commands, so you cannot combine an order operation with a mint, an unmint, or any other command input — and **one `CreateOrder` output** per transaction. Concluding and re-creating in the same transaction sits precisely at both limits, which is what makes the atomic update work.
+
+Everything past those limits is rejected by the verifier:
+
+- A second command input (order operation or token account command) — `MultipleAccountCommands`.
+- A second `CreateOrder` output — `MultipleOrdersCreated`.
+- Spending a `CreateOrder` output as a UTXO input — orders are not UTXOs; they are consumed only through order operation inputs (`InvalidInputTypeInTx`).
+- Creating an order in a transaction with no UTXO input — the new order's id is derived from the first UTXO outpoint (`NoUtxoInputsForOrderIdCreation`), which is why the fee input below is not optional.
+
+:::
+
+## The ingredients
+
+| Piece | Encoder | Source of truth |
+| ----- | ------- | --------------- |
+| Conclude input | `encode_input_for_conclude_order(orderId, nonce, currentBlockHeight, network)` | `nonce` from the indexer's order record |
+| New order output | `encode_create_order_output(askAmount, askTokenId, giveAmount, giveTokenId, concludeAddress, network)` | the new conditions |
+| Surplus / change outputs | `encode_output_transfer` (coins) / `encode_output_token_transfer` (tokens) | whatever is not re-locked |
+| Fee input | `encode_input_for_utxo` with `encode_outpoint_source_id` | a coin UTXO you own |
+| Witness for the conclude input | `encode_witness` with a `TxAdditionalInfo` | the order's balances |
+| Witness for the coin input | `encode_witness` | the fee UTXO's output |
+| New order id | `get_order_id(inputs, network)` | predicted from the inputs |
+
+## Fetch the order state
+
+```ts
+import { MintlayerApiProvider } from '@mintlayer/sdk';
+
+const api = new MintlayerApiProvider('http://127.0.0.1:3000', 'http://127.0.0.1:3000'); // the second URL is only used for batched queries
+
+const oldOrderId = 'ord1...'; // the order to update
+const order = await api.getOrder(oldOrderId);
+const tip = await api.getChainTip();
+console.log(`give: ${order.give_balance.atoms} ask: ${order.ask_balance.atoms} nonce: ${order.nonce}`);
+```
+
+Balances come back as `{atoms, decimal}` string pairs — always compose with atoms. `initially_asked` / `initially_given` capture what the maker originally promised; the sighash for the conclude input is bound to them, so keep them exactly as returned.
+
+## Build the transaction
+
+```ts
+import initWasm, {
+ Amount,
+ encode_create_order_output,
+ encode_input_for_conclude_order,
+ encode_input_for_utxo,
+ encode_outpoint_source_id,
+ encode_output_transfer,
+ encode_transaction,
+ Network,
+ SourceId,
+} from '@mintlayer/wasm-lib';
+
+await initWasm(); // required once, before any encoder call
+
+const network = Network.Mainnet;
+const concatBytes = (...parts: Uint8Array[]) => {
+ const total = parts.reduce((n, p) => n + p.length, 0);
+ const out = new Uint8Array(total);
+ let offset = 0;
+ for (const p of parts) {
+ out.set(p, offset);
+ offset += p.length;
+ }
+ return out;
+};
+
+// 1. Consume the old order
+const concludeInput = encode_input_for_conclude_order(
+ oldOrderId,
+ BigInt(order.nonce),
+ BigInt(tip.block_height) + 1n, // predicted inclusion height (tip + 1)
+ network,
+);
+
+// 2. Fee payer: a coin UTXO you own
+const feeTxIdBytes = Uint8Array.from(feeTxId.match(/.{2}/g)!.map((b) => parseInt(b, 16)));
+const feeInput = encode_input_for_utxo(
+ encode_outpoint_source_id(feeTxIdBytes, SourceId.Transaction),
+ 0,
+);
+
+// 3. Re-create the order with updated parameters
+const newOrderOutput = encode_create_order_output(
+ Amount.from_atoms('12000000000000'), // ask 12 coins
+ null, // null token id = coin
+ Amount.from_atoms('15000000000'), // give 150 tokens
+ giveTokenId,
+ order.conclude_destination, // where the concluded leftovers return
+ network,
+);
+
+// 4. Any balance not re-locked returns via change outputs
+const changeOutput = encode_output_transfer(
+ Amount.from_atoms('2000000000'),
+ makerAddr,
+ network,
+);
+
+const tx = encode_transaction(
+ concatBytes(concludeInput, feeInput),
+ concatBytes(newOrderOutput, changeOutput),
+ 0n,
+);
+```
+
+If the fee UTXO came from a token transfer instead of a coin transfer, the fee input still needs a **coin** UTXO: fees are always paid in the base coin. Keep a separate small coin UTXO for fees and use the change output to sweep back anything you do not want to re-lock.
+
+## Sign it
+
+The conclude input's sighash requires the order's balances via `TxAdditionalInfo`: this is the part that differs from a plain transfer. Signing is also where the inputs' backing UTXOs come in: `encode_witness` takes one entry per input, `0x00` for non-UTXO inputs (the conclude input) and `0x01` plus the encoded output for UTXO inputs (the fee input):
+
+```ts
+import {
+ encode_witness,
+ encode_signed_transaction,
+ encode_output_transfer,
+ make_private_key,
+ SignatureHashType,
+ type TxAdditionalInfo,
+} from '@mintlayer/wasm-lib';
+
+const makerKeyBytes = make_private_key(); // 32 random bytes — load yours instead
+const feeOutput = encode_output_transfer(feeAmountAtoms, makerAddr, network); // re-encode the fee UTXO's output
+const inputUtxos = concatBytes(new Uint8Array([0x00, 0x01]), feeOutput);
+
+const additionalInfo: TxAdditionalInfo = {
+ pool_info: {},
+ order_info: {
+ [oldOrderId]: {
+ initially_asked: { coins: { atoms: order.initially_asked.atoms } },
+ initially_given: { tokens: { token_id: giveTokenId, amount: { atoms: order.initially_given.atoms } } },
+ ask_balance: { atoms: order.ask_balance.atoms },
+ give_balance: { atoms: order.give_balance.atoms },
+ },
+ },
+};
+
+let witnessBytes = new Uint8Array();
+for (let i = 0; i < 2; i++) { // one witness per input, in input order
+ witnessBytes = concatBytes(
+ witnessBytes,
+ encode_witness(
+ SignatureHashType.ALL,
+ makerKeyBytes,
+ makerAddr,
+ tx,
+ inputUtxos,
+ i,
+ additionalInfo, // required for the conclude input
+ BigInt(tip.block_height) + 1n,
+ network,
+ ),
+ );
+}
+
+const signedTx = encode_signed_transaction(tx, witnessBytes);
+```
+
+## Submit and name the new order
+
+```ts
+import { get_order_id } from '@mintlayer/wasm-lib';
+
+const txHex = Array.from(signedTx, (b) => b.toString(16).padStart(2, '0')).join('');
+await api.broadcastTransaction(txHex);
+
+const newOrderId = get_order_id(concatBytes(concludeInput, feeInput), network);
+console.log('your updated order:', newOrderId);
+```
+
+`get_order_id` predicts the id the same way consensus derives it — from the first UTXO outpoint — so you can index the new order immediately.
+
+## Variations
+
+- **Cancel instead of update**: keep the conclude input and the fee input, drop the `CreateOrder` output, and transfer everything back. One order operation input, zero created orders — allowed.
+- **Self-fill then re-quote**: fill your own order in one transaction (`FillOrder` input + `encode_witness_no_signature` for the witness of the order input) and conclude-and-recreate another; just never combine two order operations in the same transaction.
+- **Chain delegations the same way**: the spend-and-recreate pattern applies to delegation outputs too; see [Staking with the wasm module](../go/staking.md#manual-transaction-building) for the Go twin.
+
+For the higher-level wrappers around these encoders, see [Transactions](../../build/sdks/javascript/transactions.md#manual-building).
diff --git a/docs/guides/python/index.md b/docs/guides/python/index.md
index 3de2ca3..327f96e 100644
--- a/docs/guides/python/index.md
+++ b/docs/guides/python/index.md
@@ -19,3 +19,4 @@ The wallet-cli versions of the same workflows live in the [command-line guides](
| [Atomic swap](atomic-swap.md) | The Mintlayer side of a BTC ⇄ ML swap with HTLCs |
| [Wallet & signing (WASM)](wallet-wasm.md) | Generate wallets, derive addresses, sign without a daemon |
| [Forging custom transactions](custom-transactions.md) | Mixed inputs/outputs, protocol fees, multi-party signing, intents |
+| [Composing UTXOs](utxo-composition.md) | Update an order atomically: conclude input + create output |
diff --git a/docs/guides/python/trading-with-orders.md b/docs/guides/python/trading-with-orders.md
index 63bd108..ed9ee7b 100644
--- a/docs/guides/python/trading-with-orders.md
+++ b/docs/guides/python/trading-with-orders.md
@@ -83,4 +83,4 @@ Concluding returns the unclaimed `give` remainder plus any accumulated `ask` bal
## Manual flows
-With the `wasm` module, `encode_create_order_output` builds the order output and `get_order_id` predicts the id from the inputs; fills use `encode_input_for_fill_order`, whose inputs must **not** be signed (`encode_witness_no_signature`). See [Tokens and NFTs](../../build/sdks/python/tokens.md) in the SDK reference for the encoders.
+With the `wasm` module, `encode_create_order_output` builds the order output and `get_order_id` predicts the id from the inputs; fills use `encode_input_for_fill_order`, whose inputs must **not** be signed (`encode_witness_no_signature`). To **update** an existing order (new price or amounts), compose a transaction that consumes the conclude-order input and re-creates the order: see [Composing UTXOs](utxo-composition.md). See [Tokens and NFTs](../../build/sdks/python/tokens.md) in the SDK reference for the encoders.
diff --git a/docs/guides/python/utxo-composition.md b/docs/guides/python/utxo-composition.md
new file mode 100644
index 0000000..e6d5578
--- /dev/null
+++ b/docs/guides/python/utxo-composition.md
@@ -0,0 +1,161 @@
+---
+title: "Composing UTXOs: Updating an Order"
+description: "How to compose UTXOs on Mintlayer: update a DEX order atomically by consuming its conclude-order input and re-creating it with a create-order output."
+sidebar_position: 9
+---
+
+# Composing UTXOs: Updating an Order
+
+Mintlayer is a UTXO chain: **there is no mutable state**. A DEX order is an unspent output, and every protocol object behaves the same way, so "updating" an order means spending it and re-creating it *in the same transaction*. This atomic spend-and-recreate pattern is the essence of UTXO composition.
+
+## The pattern
+
+An order lives as a `CreateOrder` output holding the `give` balance plus any accumulated `ask` balance. Its spend authority is the maker, gated by the order's **nonce**. To update the order (new price, new amounts), build one transaction that:
+
+1. consumes the order via a **conclude-order input** (this releases the whole order balance), and
+2. re-locks the funds into a fresh **create-order output** with the new parameters.
+
+```mermaid
+flowchart LR
+ OLD["Order UTXO
(give 100, ask 0)"] -->|"conclude-order input
(nonce n)"| TX["Transaction"]
+ FEE["Coin UTXO
(fee payer)"] --> TX
+ TX -->|"create-order output
(give 100, ask 20)"| NEW["New order UTXO
(new order id)"]
+ TX -->|"change"| MAKER["Maker address"]
+```
+
+Two consequences worth internalizing:
+
+- The **new order gets a new ID** (IDs are derived from inputs; see [predicting IDs](custom-transactions.md#predicting-ids)). Publish the new ID and retire the old one.
+- If the transaction fails or is dropped, nothing changed: the old order is still live and its nonce unspent.
+
+:::note[What consensus allows and forbids]
+
+The pattern above is exactly what consensus permits: **one order operation input** (fill, conclude, or freeze) — a slot shared with token account commands, so you cannot combine an order operation with a mint, an unmint, or any other command input — and **one `CreateOrder` output** per transaction. Concluding and re-creating in the same transaction sits precisely at both limits, which is what makes the atomic update work.
+
+Everything past those limits is rejected by the verifier:
+
+- A second command input (order operation or token account command) — `MultipleAccountCommands`.
+- A second `CreateOrder` output — `MultipleOrdersCreated`.
+- Spending a `CreateOrder` output as a UTXO input — orders are not UTXOs; they are consumed only through order operation inputs (`InvalidInputTypeInTx`).
+- Creating an order in a transaction with no UTXO input — the new order's id is derived from the first UTXO outpoint (`NoUtxoInputsForOrderIdCreation`), which is why the fee input below is not optional.
+
+:::
+
+## The ingredients
+
+| Piece | Encoder | Source of truth |
+| ----- | ------- | --------------- |
+| Conclude input | `encode_input_for_conclude_order(order_id, nonce, current_block_height, network)` | `nonce` from the indexer's order record |
+| New order output | `encode_create_order_output(ask_amount, ask_token_id, give_amount, give_token_id, conclude_address, network)` | Your new price/amounts |
+| Surplus / change outputs | `encode_output_transfer` / `encode_output_token_transfer` | Old balance minus new give amount |
+| Fee input | `encode_input_for_utxo` | A coin UTXO you own |
+| Witness for the conclude input | `encode_witness` with `TxAdditionalInfo` | Order balances from the indexer |
+| Witness for the coin input | `encode_witness` | n/a |
+
+## Fetch the order state
+
+```python
+from mintlayer.indexer import Client
+
+idx = Client("http://127.0.0.1:3000")
+
+order = idx.get_order(old_order_id)
+print(f"give: {order.give_balance.atoms} ask: {order.ask_balance.atoms} nonce: {order.nonce}")
+```
+
+The `ask_balance` matters: any asks accumulated from takers are part of the order you are concluding, so the new order (or a change output) must account for them.
+
+## Build the transaction
+
+Example: top up the `give` side and raise the ask. Old order: give 100 tokens / ask 10 coins. New order: give 150 / ask 12. The conclude releases 100 tokens plus accumulated asks; a separate token UTXO adds the extra 50.
+
+```python
+# 1. Consume the old order
+conclude_input = c.encode_input_for_conclude_order(
+ old_order_id, order.nonce, tip.block_height, Network.MAINNET
+)
+
+# 2. Fee payer
+src_id = c.encode_outpoint_source_id(fee_txid_bytes, SOURCE_TRANSACTION)
+fee_input = c.encode_input_for_utxo(src_id, 0)
+
+# 3. Re-create the order with updated parameters
+new_order_output = c.encode_create_order_output(
+ Amount(atoms="12"), None, # ask 12 coins (None token id = coin)
+ Amount(atoms="150"), give_token_id, # give 150 tokens
+ conclude_address, # same conclude destination
+ Network.MAINNET,
+)
+
+# 4. Any balance not re-locked returns via change outputs
+change_output = c.encode_output_transfer(
+ Amount(atoms="2000000000"), maker_addr, Network.MAINNET # fee change
+)
+
+tx = c.encode_transaction(conclude_input + fee_input, new_order_output + change_output, 0)
+```
+
+Token inputs (e.g. a UTXO holding the additional 50 tokens) encode with `encode_input_for_utxo` like any other spend; only the *outputs* distinguish coin from token value.
+
+## Sign it
+
+The conclude input's sighash requires the order's balances via `TxAdditionalInfo`; this is the part that differs from a plain transfer. Signing is also where the inputs' backing UTXOs come in: `encode_witness` takes one entry per input, `b"\x00"` for non-UTXO inputs (the conclude input) and `b"\x01"` plus the encoded output for UTXO inputs (the fee input):
+
+```python
+fee_output = c.encode_output_transfer(fee_amount, maker_addr, Network.MAINNET) # re-encode the fee UTXO's output
+input_utxos = b"\x00\x01" + fee_output
+
+from mintlayer.wasm import OrderBalance, OrderInfo, SimpleCurrencyAmount, TxAdditionalInfo
+
+additional_info = TxAdditionalInfo(
+ order_info={
+ old_order_id: OrderInfo(
+ initially_asked=SimpleCurrencyAmount.coins(order.initially_asked.atoms),
+ initially_given=SimpleCurrencyAmount.tokens(
+ give_token_id, order.initially_given.atoms
+ ),
+ ask_balance=OrderBalance(atoms=order.ask_balance.atoms),
+ give_balance=OrderBalance(atoms=order.give_balance.atoms, token_id=give_token_id),
+ )
+ }
+)
+
+witness_bytes = b""
+for i in range(2): # one witness per input, in input order
+ witness_bytes += c.encode_witness(
+ SIGHASH_ALL,
+ maker_key,
+ maker_addr,
+ tx,
+ input_utxos,
+ i,
+ additional_info, # required for the order input
+ tip.block_height,
+ Network.MAINNET,
+ )
+
+signed_tx = c.encode_signed_transaction(tx, witness_bytes)
+```
+
+Submit via the indexer or node as usual, then predict and store the new order ID:
+
+```python
+new_order_id = c.get_order_id(conclude_input + fee_input, Network.MAINNET)
+```
+
+## Variations of the same pattern
+
+The conclude-then-recreate transaction is one instance of a general tool. With the same encoders you can:
+
+- **Cancel an order**: conclude input, no create-order output; the balance flows to change outputs.
+- **Self-fill and requote**: conclude input + `FillOrder`-style split: partially fill your own order and re-create a smaller one. (A pure fill by a taker uses `encode_input_for_fill_order` and needs **no signature** for that input: `encode_witness_no_signature`.)
+- **Freeze before restructuring**: `encode_input_for_freeze_order` stops takers while you prepare the replacement transaction.
+- **Chain protocol objects**: the same spend-and-recreate logic composes delegations (withdraw input + `encode_output_delegate_staking` output) and any future output type.
+
+:::note
+
+Exactly which balances must be re-assigned to which outputs is enforced by consensus, not by the encoders: the runtime tells you *if* a transaction is invalid, the indexer tells you *what* the balances are, and the composition is yours to design.
+
+:::
+
+For the order model itself (give/ask, conclude key, freeze semantics), see [Trading with Orders](trading-with-orders.md) and the wallet-cli guide [Trading with orders](../cli/trading-with-orders.md).
diff --git a/docs/guides/rust/index.md b/docs/guides/rust/index.md
index 12c306d..e630120 100644
--- a/docs/guides/rust/index.md
+++ b/docs/guides/rust/index.md
@@ -16,6 +16,7 @@ The wallet-cli versions of the same workflows live in the [command-line guides](
| [Issue an NFT](issue-nft.md) | MLS-03 issuance and transfers |
| [Staking and delegation](staking.md) | Pool lifecycle and delegations |
| [Trading with orders](trading-with-orders.md) | On-chain orders: create, discover, fill, conclude |
+| [Composing UTXOs](utxo-composition.md) | Update an order atomically: conclude input + create output |
| [Atomic swap](atomic-swap.md) | The Mintlayer side of a BTC ⇄ ML swap with HTLCs |
For full-custody transaction building beyond these recipes, see [Building transactions](../../build/sdks/rust/transactions.md) in the SDK reference; runnable programs live in the repository under [examples/](https://github.com/mintlayer/rust-sdk/tree/main/examples).
diff --git a/docs/guides/rust/trading-with-orders.md b/docs/guides/rust/trading-with-orders.md
index ba26534..68d77e2 100644
--- a/docs/guides/rust/trading-with-orders.md
+++ b/docs/guides/rust/trading-with-orders.md
@@ -138,6 +138,6 @@ c.freeze_order(FreezeOrderParams {
Concluding returns the unclaimed `give` remainder plus any accumulated `ask` balance to the order's conclude destination. Only the maker can freeze or conclude.
-## Manual flows
+To **update** an existing order (new price or amounts), compose a transaction that consumes the conclude-order input and re-creates the order: see [Composing UTXOs](utxo-composition.md).
With the `crypto` feature, `encode_create_order_output` builds the order output and `get_order_id` predicts the id from the inputs; fills use `encode_input_for_fill_order`, whose inputs must **not** be signed (`encode_witness_no_signature`). Before the orders V1 fork the fill's `nonce` and `destination` parameters are significant; after the fork both are ignored, and `encode_input_for_freeze_order` requires orders V1 (`crypto::Error::OrdersV1NotActivated` before it). See [Tokens and NFTs](../../build/sdks/rust/tokens.md#dex-orders) in the SDK reference.
diff --git a/docs/guides/rust/utxo-composition.md b/docs/guides/rust/utxo-composition.md
new file mode 100644
index 0000000..7b5604c
--- /dev/null
+++ b/docs/guides/rust/utxo-composition.md
@@ -0,0 +1,196 @@
+---
+title: "Composing UTXOs: Updating an Order"
+description: "How to compose UTXOs in Rust: update a DEX order atomically by consuming its conclude-order input and re-creating it with a create-order output."
+sidebar_position: 9
+---
+
+# Composing UTXOs: Updating an Order
+
+Mintlayer is a UTXO chain: **there is no mutable state**. A DEX order is an unspent output, and every protocol object behaves the same way, so "updating" an order means spending it and re-creating it *in the same transaction*. This atomic spend-and-recreate pattern is the essence of UTXO composition.
+
+This guide uses the [crypto module](../../build/sdks/rust/crypto.md) of the Rust SDK and its indexer client. The same workflow in Go, JavaScript, and Python lives in the [Go](../go/utxo-composition.md), [JavaScript](../javascript/utxo-composition.md), and [Python](../python/utxo-composition.md) guides.
+
+:::note[Requires an unreleased SDK]
+
+The order-signing re-exports used below (`OrderAdditionalInfo`, `OrderId`, `TokenId`, and the public `parse_addressable`) ship in the next SDK release after v0.1.0 ([mintlayer/rust-sdk#5](https://github.com/mintlayer/rust-sdk/pull/5), unreleased at the time of writing).
+
+:::
+
+## The pattern
+
+A maker wants to update an existing order's conditions: lower the `ask` from 10 coins to 12 coins, raise the `give` from 100 tokens to 150 tokens. The transaction must contain:
+
+```mermaid
+flowchart LR
+ C["Conclude order input
(consumes the old order)"] --> N["Transaction"]
+ F["Fee input
(UTXO paying the fee)"] --> N
+ N -->|"create-order output
(give 150, ask 12)"| O["New order"]
+ N -->|"change"| R["Maker address"]
+```
+
+All four effects happen atomically. If the transaction fails, the old order is untouched.
+
+:::note[What consensus allows and forbids]
+
+The pattern above is exactly what consensus permits: **one order operation input** (fill, conclude, or freeze) — a slot shared with token account commands, so you cannot combine an order operation with a mint, an unmint, or any other command input — and **one `CreateOrder` output** per transaction. Concluding and re-creating in the same transaction sits precisely at both limits, which is what makes the atomic update work.
+
+Everything past those limits is rejected by the verifier:
+
+- A second command input (order operation or token account command) — `MultipleAccountCommands`.
+- A second `CreateOrder` output — `MultipleOrdersCreated`.
+- Spending a `CreateOrder` output as a UTXO input — orders are not UTXOs; they are consumed only through order operation inputs (`InvalidInputTypeInTx`).
+- Creating an order in a transaction with no UTXO input — the new order's id is derived from the first UTXO outpoint (`NoUtxoInputsForOrderIdCreation`), which is why the fee input below is not optional.
+
+:::
+
+## The ingredients
+
+| Piece | Call | Source of truth |
+| ----- | ---- | --------------- |
+| Conclude input | `encode_input_for_conclude_order(order_id, nonce, current_block_height, network)` | `nonce` from the indexer's order record |
+| New order output | `encode_create_order_output(ask_amount, ask_token_id, give_amount, give_token_id, conclude_address, network)` | the new conditions |
+| Surplus / change outputs | `encode_output_transfer` (coins) / `encode_output_token_transfer` (tokens) | whatever is not re-locked |
+| Fee input | `encode_input_for_utxo` with `encode_outpoint_source_id` | a coin UTXO you own |
+| Witness for the conclude input | `encode_witness` with a `TxAdditionalInfo` | the order's balances |
+| Witness for the coin input | `encode_witness` | the fee UTXO's output |
+| New order id | `get_order_id(&inputs, network)` | predicted from the inputs |
+
+## Fetch the order state
+
+```rust
+use mintlayer_sdk::indexer;
+
+let indexer = indexer::Client::new("http://127.0.0.1:3000");
+let network = Network::Testnet;
+
+let order = indexer.order(OLD_ORDER_ID).await?;
+let tip = indexer.tip().await?;
+println!(
+ "give: {} ask: {} nonce: {}",
+ order.give_balance.atoms, order.ask_balance.atoms, order.nonce.0
+);
+
+// Predicted inclusion height: the next block after the current tip.
+let inclusion_height = tip.block_height.checked_add(1).expect("height overflow");
+```
+
+Balances come back as `Amount` — an atom count (u128) plus a `decimal` string — always compose with atoms. `initially_asked` / `initially_given` capture what the maker originally promised; the sighash for the conclude input is bound to them, so keep them exactly as returned.
+
+## Build the transaction
+
+```rust
+use mintlayer_sdk::crypto::{
+ self,
+ types::{H256, TokenId},
+ Amount, SourceId,
+};
+
+// 1. Consume the old order
+let conclude_input = crypto::encode_input_for_conclude_order(
+ OLD_ORDER_ID,
+ order.nonce.0,
+ inclusion_height,
+ network,
+)?;
+
+// 2. Fee payer: a coin UTXO you own
+let fee_tx_id = H256::from_slice(&hex::decode(FEE_TX_ID_HEX)?);
+let fee_input = crypto::encode_input_for_utxo(
+ crypto::encode_outpoint_source_id(fee_tx_id, SourceId::Transaction),
+ 0,
+);
+
+// 3. Re-create the order with updated parameters
+let give_token_id: TokenId = crypto::parse_addressable(network, GIVE_TOKEN_ID)?;
+let new_order_output = crypto::encode_create_order_output(
+ Amount::from_atoms(12_000_000_000_000), // ask 12 coins
+ None, // None = coin
+ Amount::from_atoms(15_000_000_000), // give 150 tokens
+ Some(GIVE_TOKEN_ID),
+ &order.conclude_destination,
+ network,
+)?;
+
+// 4. Any balance not re-locked returns via change outputs
+let change_output =
+ crypto::encode_output_transfer(Amount::from_atoms(2_000_000_000), MAKER, network)?;
+
+let inputs = vec![conclude_input.clone(), fee_input];
+let transaction =
+ crypto::encode_transaction(inputs.clone(), vec![new_order_output, change_output], 0)?;
+```
+
+If the fee UTXO came from a token transfer instead of a coin transfer, the fee input still needs a **coin** UTXO: fees are always paid in the base coin. Keep a separate small coin UTXO for fees and use the change output to sweep back anything you do not want to re-lock.
+
+## Sign it
+
+The conclude input's sighash requires the order's balances via `TxAdditionalInfo`: this is the part that differs from a plain transfer. Signing is also where the inputs' backing UTXOs come in: `encode_witness` takes one entry per input, `None` for non-UTXO inputs (the conclude input) and `Some(output)` for UTXO inputs (the fee input):
+
+```rust
+use mintlayer_sdk::crypto::{
+ self,
+ types::{Encode, OrderId, OutputValue, TxOutput},
+ SigHashType, TxAdditionalInfo,
+};
+
+let maker = MAKER;
+let maker_key = crypto::make_private_key();
+let order_id: OrderId = crypto::parse_addressable(network, OLD_ORDER_ID)?;
+let additional_info = TxAdditionalInfo::new().with_order_info(
+ order_id,
+ crypto::OrderAdditionalInfo {
+ initially_asked: OutputValue::Coin(Amount::from_atoms(order.initially_asked.atoms)),
+ initially_given: OutputValue::TokenV1(
+ give_token_id,
+ Amount::from_atoms(order.initially_given.atoms),
+ ),
+ ask_balance: Amount::from_atoms(order.ask_balance.atoms),
+ give_balance: Amount::from_atoms(order.give_balance.atoms),
+ },
+);
+
+let input_utxos = [
+ None,
+ Some(TxOutput::Transfer(
+ OutputValue::Coin(Amount::from_atoms(2_500_000_000)), // re-encode the fee UTXO's output
+ crypto::encode_destination(maker, network)?,
+ )),
+];
+
+let mut signatures = Vec::new();
+for index in 0..inputs.len() {
+ signatures.push(crypto::encode_witness(
+ SigHashType::all(),
+ &maker_key,
+ maker,
+ &transaction,
+ &input_utxos,
+ index,
+ &additional_info,
+ inclusion_height,
+ network,
+ )?);
+}
+
+let signed = crypto::encode_signed_transaction(transaction, signatures)?;
+let signed_hex = hex::encode(signed.encode());
+```
+
+## Submit and name the new order
+
+```rust
+indexer.submit_transaction(&signed_hex).await?;
+
+let new_order_id = crypto::get_order_id(&inputs, network)?;
+println!("your updated order: {new_order_id}");
+```
+
+`get_order_id` predicts the id the same way consensus derives it — from the first UTXO outpoint — so you can index the new order immediately.
+
+## Variations
+
+- **Cancel instead of update**: keep the conclude input and the fee input, drop the `CreateOrder` output, and transfer everything back. One order operation input, zero created orders — allowed.
+- **Self-fill then re-quote**: fill your own order in one transaction (`FillOrder` input + `encode_witness_no_signature` for the witness of the order input) and conclude-and-recreate another; just never combine two order operations in the same transaction.
+- **Chain delegations the same way**: the spend-and-recreate pattern applies to delegation outputs too; see [Building transactions](../../build/sdks/rust/transactions.md) for more signed-composition flows.
+
+For every encoder in the module, see [Cryptography](../../build/sdks/rust/crypto.md).