Introduction
radix-transactions is the Rust crate that defines Radix's transaction models, the manifest builder and the manifest compiler. The Radix Engine and the node use the same crate, so a transaction it builds is the format the network validates. The Radix docs recommend it for any Rust integration, over the Rust core of the Radix Engine Toolkit, which is not published to crates.io (Rust Libraries).
It is also the most direct way to build the V2 transactions introduced at the Cuttlefish protocol update. The TypeScript toolkit, @radixdlt/radix-engine-toolkit, was at 1.0.6 in November 2026 and its type definitions contain no V2 transaction or subintent builder. The docs recommend V2 for new integrations even without subintents, because it adds expiry by timestamp, a collision-resistant intent discriminator and more assertion instructions (Transaction Building).
This guide builds a transaction that takes free test $XRD from the Stokenet faucet, submits it through the Gateway API, then repeats the transfer through a subintent. Both ran on Stokenet while this page was written.
Which crates and versions do I need?
Two crates: radix-transactions for the builders and models, and radix-common for keys, addresses and network definitions. The engine crates are published together at each protocol update and depend on each other, and they do not follow semantic versioning, so the docs advise pinning one exact version across all of them and moving it by hand (Rust Libraries). 1.4.0 was the newest release on crates.io in November 2026.
[dependencies]
radix-transactions = "=1.4.0"
radix-common = "=1.4.0"Both crates have a prelude module, and the docs recommend importing through it, because paths inside the crates move between versions.
Building and notarizing a V2 transaction
A V2 transaction has two headers. The transaction header names the notary: the key that signs the finished transaction last, sealing it. The intent header names the network and the window in which the transaction may commit. The program below builds both, adds a three-instruction manifest and notarizes it:
use radix_common::prelude::*;
use radix_transactions::prelude::*;
fn main() {
let epoch: u64 = std::env::args().nth(1).expect("current epoch").parse().unwrap();
let network = NetworkDefinition::stokenet();
// A throwaway key. Its account exists as soon as something is deposited to it.
let key = Ed25519PrivateKey::from_u64(914).unwrap();
let account = ComponentAddress::preallocated_account_from_public_key(&key.public_key());
let tx = TransactionBuilder::new_v2()
.transaction_header(TransactionHeaderV2 {
notary_public_key: key.public_key().into(),
notary_is_signatory: true,
tip_basis_points: 0,
})
.intent_header(IntentHeaderV2 {
network_id: network.id,
start_epoch_inclusive: Epoch::of(epoch),
end_epoch_exclusive: Epoch::of(epoch + 2),
min_proposer_timestamp_inclusive: None,
max_proposer_timestamp_exclusive: None,
intent_discriminator: 914 ^ epoch,
})
.manifest_builder(|builder| {
builder
.lock_fee_from_faucet()
.get_free_xrd_from_faucet()
.deposit_entire_worktop(account)
})
.notarize(&key)
.build();
let addresses = AddressBech32Encoder::new(&network);
let hashes = TransactionHashBech32Encoder::new(&network);
println!("account {}", account.to_string(&addresses));
println!("intent {}", hashes.encode(&tx.transaction_hashes.transaction_intent_hash).unwrap());
println!("payload {}", tx.raw.to_hex());
}The settings that matter:
notary_is_signatory: truelets the notary's signature count as a signature on the intent, so one key both signs and notarizes. A wallet or exchange with a single key per account uses this; a separate ephemeral notary key is the alternative, with the account keys added through.sign().- The epoch window is inclusive at the start and exclusive at the end. An epoch lasts about five minutes, so
epoch + 2leaves five to ten minutes before the transaction permanently rejects; this is the docs' recommended setting. The current epoch comes from the Gateway or Core API. intent_discriminatoris a freeu64that makes two otherwise identical intents hash differently. Production code sets it randomly.tip_basis_pointsreplaces V1's tip percentage. Zero is the normal value outside congestion (Transaction Fees).
The manifest locks the fee from the faucet component, so the new account needs no $XRD to pay for its first transaction. deposit_entire_worktop calls the account's owner-only deposit_batch, which works here because the account's own key signed. A deposit to someone else's account uses try_deposit_entire_worktop_or_abort, which respects that account's deposit rules.
build() returns a DetailedNotarizedTransactionV2 carrying the transaction, its raw bytes, the names of its buckets and proofs, and every hash. The transaction intent hash, encoded with the network's txid_ prefix, is the transaction ID the Gateway and dashboards use.
How do I submit it?
Post the hex payload to the Gateway's /transaction/submit, then poll /transaction/status with the intent hash (Gateway API reference). A node's Core API has the equivalent endpoints for integrations that run their own node (Exchange Integration).
GATEWAY=https://stokenet.radixdlt.com
EPOCH=$(curl -s -X POST $GATEWAY/status/gateway-status -H 'content-type: application/json' -d '{}' | jq .ledger_state.epoch)
./target/release/rtx $EPOCH > out.txt
curl -s -X POST $GATEWAY/transaction/submit -H 'content-type: application/json' \
-d "{\"notarized_transaction_hex\":\"$(awk '/^payload/{print $2}' out.txt)\"}"
curl -s -X POST $GATEWAY/transaction/status -H 'content-type: application/json' \
-d "{\"intent_hash\":\"$(awk '/^intent/{print $2}' out.txt)\"}"On 14 November 2026, at epoch 22518, the submission returned {"duplicate":false}. The status read Pending twice and then CommittedSuccess about nine seconds after submission. The committed details showed 10,000 $XRD leaving the faucet and arriving in the new account, and a fee of 0.58 $XRD paid by the faucet. The payload's first three bytes, 4d 22 0c, are the Manifest SBOR prefix, the enum value kind and discriminator 12, which the crate assigns to NotarizedTransactionV2 (SBOR).
A status other than success needs handling: a temporary rejection can still commit later, for example once the fee payer is topped up, while a permanent rejection or a committed failure needs a new transaction with a new intent hash (Worked Example 1).
Adding a subintent
A subintent is a signed piece of a transaction that someone else completes, the mechanism behind wallet pre-authorizations. In the builder it is a partial transaction: an intent header, a subintent manifest that must end by yielding to its parent, and its own signatures. The parent adds it with add_signed_child, which also registers its hash, so the parent manifest can call it by name:
// The child: takes free $XRD from the faucet and hands the bucket to its parent.
let child = TransactionBuilder::new_partial_v2()
.intent_header(header(epoch ^ 1))
.manifest_builder(|builder| {
builder
.get_free_xrd_from_faucet()
.take_all_from_worktop(XRD, "xrd")
.yield_to_parent_with_name_lookup(|lookup| (lookup.bucket("xrd"),))
})
.sign(&helper)
.build();
// The parent: pays the fee, runs the child, deposits what it returned.
let tx = TransactionBuilder::new_v2()
.transaction_header(TransactionHeaderV2 {
notary_public_key: notary.public_key().into(),
notary_is_signatory: true,
tip_basis_points: 0,
})
.intent_header(header(epoch ^ 2))
.add_signed_child("child", child.partial_transaction)
.manifest_builder(|builder| {
builder
.lock_fee_from_faucet()
.yield_to_child("child", ())
.deposit_entire_worktop(account)
})
.notarize(¬ary)
.build();Here header is a closure that returns the same IntentHeaderV2 as above with the discriminator it is given, and helper is a second Ed25519 key.
This transaction committed on Stokenet in about nine seconds. The faucet paid the 0.43 $XRD fee because the parent locked it, the child's call to the faucet produced the 10,000 $XRD, and the parent deposited them. A child partial transaction received from elsewhere, such as a pre-authorization a wallet signed, is passed to add_signed_child the same way after decoding it from its raw bytes.
The docs give one limit: when the dApp Toolkit sends a manifest to the wallet with sendTransaction, the wallet accepts V1 instructions only; V2 instructions reach the wallet through the pre-authorization flow (Transaction Building).
Next Steps
- Exchange Integration – sign, submit and track transfers for many accounts
- SBOR – the encoding inside every payload this guide produced
External Links
- Transaction Building – Radix documentation
- Rust Manifest Builder – the builder's methods, grouped by instruction
- Rust Libraries – which Rust crates exist and which to use
- radix-transactions on docs.rs
- radix-transactions source – radixdlt-scrypto repository
