diff --git a/bridges/bin/runtime-common/src/lib.rs b/bridges/bin/runtime-common/src/lib.rs
index e8a2d2470fa191030dc4c20ee3fd37fbd90f9c05..12b096492cd486dd78e5912263da43603c24dcb5 100644
--- a/bridges/bin/runtime-common/src/lib.rs
+++ b/bridges/bin/runtime-common/src/lib.rs
@@ -159,7 +159,21 @@ pub enum CustomNetworkId {
 	RialtoParachain,
 }
 
+impl TryFrom<bp_runtime::ChainId> for CustomNetworkId {
+	type Error = ();
+
+	fn try_from(chain: bp_runtime::ChainId) -> Result<Self, Self::Error> {
+		Ok(match chain {
+			bp_runtime::MILLAU_CHAIN_ID => Self::Millau,
+			bp_runtime::RIALTO_CHAIN_ID => Self::Rialto,
+			bp_runtime::RIALTO_PARACHAIN_CHAIN_ID => Self::RialtoParachain,
+			_ => return Err(()),
+		})
+	}
+}
+
 impl CustomNetworkId {
+	/// Converts self to XCM' network id.
 	pub const fn as_network_id(&self) -> NetworkId {
 		match *self {
 			CustomNetworkId::Millau => NetworkId::Kusama,
diff --git a/bridges/relays/bin-substrate/Cargo.toml b/bridges/relays/bin-substrate/Cargo.toml
index 7853b9cb599ac745f836a667990b92e56f9f2bda..0a315035042a13bd781ffc29788507573f4d379c 100644
--- a/bridges/relays/bin-substrate/Cargo.toml
+++ b/bridges/relays/bin-substrate/Cargo.toml
@@ -49,6 +49,9 @@ relay-utils = { path = "../utils" }
 relay-westend-client = { path = "../client-westend" }
 relay-wococo-client = { path = "../client-wococo" }
 rialto-runtime = { path = "../../bin/rialto/runtime" }
+# we are not using this runtime to craft callsour transactions, but we still need it
+# to prepare large XCM messages
+rialto-parachain-runtime = { path = "../../bin/rialto-parachain/runtime" }
 substrate-relay-helper = { path = "../lib-substrate-relay" }
 
 # Substrate Dependencies
@@ -62,8 +65,8 @@ polkadot-parachain = { git = "https://github.com/paritytech/polkadot", branch =
 polkadot-primitives = { git = "https://github.com/paritytech/polkadot", branch = "master" }
 polkadot-runtime-common = { git = "https://github.com/paritytech/polkadot", branch = "master" }
 polkadot-runtime-parachains = { git = "https://github.com/paritytech/polkadot", branch = "master" }
-xcm = { git = "https://github.com/paritytech/polkadot", branch = "master", default-features = false }
-
+xcm = { git = "https://github.com/paritytech/polkadot", branch = "master" }
+xcm-executor = { git = "https://github.com/paritytech/polkadot", branch = "master" }
 
 [dev-dependencies]
 bp-test-utils = { path = "../../primitives/test-utils" }
diff --git a/bridges/relays/bin-substrate/src/chains/millau.rs b/bridges/relays/bin-substrate/src/chains/millau.rs
index 44416195c6a4427418b5b6a93f12dffa45a874ce..4cee26255d397eb5894cf05a125e7911b8b9d318 100644
--- a/bridges/relays/bin-substrate/src/chains/millau.rs
+++ b/bridges/relays/bin-substrate/src/chains/millau.rs
@@ -18,10 +18,38 @@
 
 use crate::cli::{encode_message::CliEncodeMessage, CliChain};
 use bp_runtime::EncodedOrDecodedCall;
+use bridge_runtime_common::CustomNetworkId;
 use relay_millau_client::Millau;
 use relay_substrate_client::SimpleRuntimeVersion;
+use xcm_executor::traits::ExportXcm;
 
 impl CliEncodeMessage for Millau {
+	fn encode_wire_message(
+		target: xcm::v3::NetworkId,
+		at_target_xcm: xcm::v3::Xcm<()>,
+	) -> anyhow::Result<Vec<u8>> {
+		anyhow::ensure!(
+			[
+				CustomNetworkId::Rialto.as_network_id(),
+				CustomNetworkId::RialtoParachain.as_network_id()
+			]
+			.contains(&target),
+			anyhow::format_err!("Unsupported target chain: {:?}", target)
+		);
+
+		Ok(millau_runtime::xcm_config::ToRialtoOrRialtoParachainSwitchExporter::validate(
+			target,
+			0,
+			&mut Some(Self::dummy_universal_source()?),
+			&mut Some(target.into()),
+			&mut Some(at_target_xcm),
+		)
+		.map_err(|e| anyhow::format_err!("Failed to prepare outbound message: {:?}", e))?
+		.0
+		 .1
+		 .0)
+	}
+
 	fn encode_execute_xcm(
 		message: xcm::VersionedXcm<Self::Call>,
 	) -> anyhow::Result<EncodedOrDecodedCall<Self::Call>> {
diff --git a/bridges/relays/bin-substrate/src/chains/rialto.rs b/bridges/relays/bin-substrate/src/chains/rialto.rs
index 34a448ae4cb13cae36a8a3b795fec09c3c2b0dba..30bc7eb13ca99370c5c769ead96bac0d42a0b686 100644
--- a/bridges/relays/bin-substrate/src/chains/rialto.rs
+++ b/bridges/relays/bin-substrate/src/chains/rialto.rs
@@ -18,10 +18,33 @@
 
 use crate::cli::{encode_message::CliEncodeMessage, CliChain};
 use bp_runtime::EncodedOrDecodedCall;
+use bridge_runtime_common::CustomNetworkId;
 use relay_rialto_client::Rialto;
 use relay_substrate_client::SimpleRuntimeVersion;
+use xcm_executor::traits::ExportXcm;
 
 impl CliEncodeMessage for Rialto {
+	fn encode_wire_message(
+		target: xcm::v3::NetworkId,
+		at_target_xcm: xcm::v3::Xcm<()>,
+	) -> anyhow::Result<Vec<u8>> {
+		anyhow::ensure!(
+			target == CustomNetworkId::Millau.as_network_id(),
+			anyhow::format_err!("Unsupported target chain: {:?}", target)
+		);
+
+		Ok(rialto_runtime::millau_messages::ToMillauBlobExporter::validate(
+			target,
+			0,
+			&mut Some(Self::dummy_universal_source()?),
+			&mut Some(target.into()),
+			&mut Some(at_target_xcm),
+		)
+		.map_err(|e| anyhow::format_err!("Failed to prepare outbound message: {:?}", e))?
+		.0
+		 .0)
+	}
+
 	fn encode_execute_xcm(
 		message: xcm::VersionedXcm<Self::Call>,
 	) -> anyhow::Result<EncodedOrDecodedCall<Self::Call>> {
diff --git a/bridges/relays/bin-substrate/src/chains/rialto_parachain.rs b/bridges/relays/bin-substrate/src/chains/rialto_parachain.rs
index 8ea2c1ffd433cdec79de5306aa11f5102e7d77e1..872d96981d0bf621363a703d6de8904b28b08cb3 100644
--- a/bridges/relays/bin-substrate/src/chains/rialto_parachain.rs
+++ b/bridges/relays/bin-substrate/src/chains/rialto_parachain.rs
@@ -18,10 +18,33 @@
 
 use crate::cli::{encode_message::CliEncodeMessage, CliChain};
 use bp_runtime::EncodedOrDecodedCall;
+use bridge_runtime_common::CustomNetworkId;
 use relay_rialto_parachain_client::RialtoParachain;
 use relay_substrate_client::SimpleRuntimeVersion;
+use xcm_executor::traits::ExportXcm;
 
 impl CliEncodeMessage for RialtoParachain {
+	fn encode_wire_message(
+		target: xcm::v3::NetworkId,
+		at_target_xcm: xcm::v3::Xcm<()>,
+	) -> anyhow::Result<Vec<u8>> {
+		anyhow::ensure!(
+			target == CustomNetworkId::Millau.as_network_id(),
+			anyhow::format_err!("Unsupported target chain: {:?}", target)
+		);
+
+		Ok(rialto_parachain_runtime::millau_messages::ToMillauBlobExporter::validate(
+			target,
+			0,
+			&mut Some(Self::dummy_universal_source()?),
+			&mut Some(target.into()),
+			&mut Some(at_target_xcm),
+		)
+		.map_err(|e| anyhow::format_err!("Failed to prepare outbound message: {:?}", e))?
+		.0
+		 .0)
+	}
+
 	fn encode_execute_xcm(
 		message: xcm::VersionedXcm<Self::Call>,
 	) -> anyhow::Result<EncodedOrDecodedCall<Self::Call>> {
diff --git a/bridges/relays/bin-substrate/src/cli/encode_message.rs b/bridges/relays/bin-substrate/src/cli/encode_message.rs
index 9abf8b2df6dda4ad7e7833c415a087b5c3b6d219..646075e8326798a10d3713614f1e8be2ff4079fe 100644
--- a/bridges/relays/bin-substrate/src/cli/encode_message.rs
+++ b/bridges/relays/bin-substrate/src/cli/encode_message.rs
@@ -16,10 +16,12 @@
 
 use crate::cli::{ExplicitOrMaximal, HexBytes};
 use bp_runtime::EncodedOrDecodedCall;
+use bridge_runtime_common::CustomNetworkId;
 use codec::Encode;
 use frame_support::weights::Weight;
 use relay_substrate_client::Chain;
 use structopt::StructOpt;
+use xcm::latest::prelude::*;
 
 /// All possible messages that may be delivered to generic Substrate chain.
 ///
@@ -43,6 +45,22 @@ pub enum Message {
 pub type RawMessage = Vec<u8>;
 
 pub trait CliEncodeMessage: Chain {
+	/// Returns dummy `AccountId32` universal source given this network id.
+	fn dummy_universal_source() -> anyhow::Result<xcm::v3::Junctions> {
+		use xcm::v3::prelude::*;
+
+		let this_network = CustomNetworkId::try_from(Self::ID)
+			.map(|n| n.as_network_id())
+			.map_err(|_| anyhow::format_err!("Unsupported chain: {:?}", Self::ID))?;
+		Ok(X2(
+			GlobalConsensus(this_network),
+			AccountId32 { network: Some(this_network), id: [0u8; 32] },
+		))
+	}
+
+	/// Returns XCM blob that is passed to the `send_message` function of the messages pallet
+	/// and then is sent over the wire.
+	fn encode_wire_message(target: NetworkId, at_target_xcm: Xcm<()>) -> anyhow::Result<Vec<u8>>;
 	/// Encode an `execute` XCM call of the XCM pallet.
 	fn encode_execute_xcm(
 		message: xcm::VersionedXcm<Self::Call>,
@@ -56,41 +74,52 @@ pub trait CliEncodeMessage: Chain {
 }
 
 /// Encode message payload passed through CLI flags.
-pub(crate) fn encode_message<Source: Chain, Target: Chain>(
+pub(crate) fn encode_message<Source: CliEncodeMessage, Target: Chain>(
 	message: &Message,
 ) -> anyhow::Result<RawMessage> {
 	Ok(match message {
 		Message::Raw { ref data } => data.0.clone(),
 		Message::Sized { ref size } => {
-			let expected_xcm_size = match *size {
+			let destination = CustomNetworkId::try_from(Target::ID)
+				.map(|n| n.as_network_id())
+				.map_err(|_| anyhow::format_err!("Unsupported target chain: {:?}", Target::ID))?;
+			let expected_size = match *size {
 				ExplicitOrMaximal::Explicit(size) => size,
 				ExplicitOrMaximal::Maximal => compute_maximal_message_size(
 					Source::max_extrinsic_size(),
 					Target::max_extrinsic_size(),
 				),
-			};
-
-			// there's no way to craft XCM of the given size - we'll be using `ExpectPallet`
-			// instruction, which has byte vector inside
-			let mut current_vec_size = expected_xcm_size;
-			let xcm = loop {
-				let xcm = xcm::VersionedXcm::<()>::V3(
-					vec![xcm::v3::Instruction::ExpectPallet {
-						index: 0,
-						name: vec![42; current_vec_size as usize],
-						module_name: vec![],
-						crate_major: 0,
-						min_crate_minor: 0,
-					}]
-					.into(),
-				);
-				if xcm.encode().len() <= expected_xcm_size as usize {
-					break xcm
-				}
-
-				current_vec_size -= 1;
-			};
-			xcm.encode()
+			} as usize;
+
+			let at_target_xcm = vec![ExpectPallet {
+				index: 0,
+				name: vec![42; expected_size],
+				module_name: vec![],
+				crate_major: 0,
+				min_crate_minor: 0,
+			}]
+			.into();
+			let at_target_xcm_size =
+				Source::encode_wire_message(destination, at_target_xcm)?.encoded_size();
+			let at_target_xcm_overhead = at_target_xcm_size.saturating_sub(expected_size);
+			let at_target_xcm = vec![ExpectPallet {
+				index: 0,
+				name: vec![42; expected_size.saturating_sub(at_target_xcm_overhead)],
+				module_name: vec![],
+				crate_major: 0,
+				min_crate_minor: 0,
+			}]
+			.into();
+
+			xcm::VersionedXcm::<()>::V3(
+				vec![ExportMessage {
+					network: destination,
+					destination: destination.into(),
+					xcm: at_target_xcm,
+				}]
+				.into(),
+			)
+			.encode()
 		},
 	})
 }
@@ -108,11 +137,7 @@ pub(crate) fn compute_maximal_message_size(
 		bridge_runtime_common::messages::target::maximal_incoming_message_size(
 			maximal_target_extrinsic_size,
 		);
-	if maximal_message_size > maximal_source_extrinsic_size {
-		maximal_source_extrinsic_size
-	} else {
-		maximal_message_size
-	}
+	std::cmp::min(maximal_message_size, maximal_source_extrinsic_size)
 }
 
 #[cfg(test)]
@@ -123,13 +148,21 @@ mod tests {
 	use relay_millau_client::Millau;
 	use relay_rialto_client::Rialto;
 
+	fn approximate_message_size<Source: CliEncodeMessage>(xcm_msg_len: usize) -> usize {
+		xcm_msg_len + Source::dummy_universal_source().unwrap().encoded_size()
+	}
+
 	#[test]
 	fn encode_explicit_size_message_works() {
 		let msg = encode_message::<Rialto, Millau>(&Message::Sized {
 			size: ExplicitOrMaximal::Explicit(100),
 		})
 		.unwrap();
-		assert_eq!(msg.len(), 100);
+		// since it isn't the returned XCM what is sent over the wire, we can only check if
+		// it is close to what we need
+		assert!(
+			(1f64 - (approximate_message_size::<Rialto>(msg.len()) as f64) / 100_f64).abs() < 0.1
+		);
 		// check that it decodes to valid xcm
 		let _ = decode_xcm::<()>(msg).unwrap();
 	}
@@ -144,7 +177,12 @@ mod tests {
 		let msg =
 			encode_message::<Rialto, Millau>(&Message::Sized { size: ExplicitOrMaximal::Maximal })
 				.unwrap();
-		assert_eq!(msg.len(), maximal_size as usize);
+		// since it isn't the returned XCM what is sent over the wire, we can only check if
+		// it is close to what we need
+		assert!(
+			(1f64 - approximate_message_size::<Rialto>(msg.len()) as f64 / maximal_size as f64)
+				.abs() < 0.1
+		);
 		// check that it decodes to valid xcm
 		let _ = decode_xcm::<()>(msg).unwrap();
 	}