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..9249958c286b53619e0aec53f18273377065fde4 100644
--- a/bridges/relays/bin-substrate/src/chains/millau.rs
+++ b/bridges/relays/bin-substrate/src/chains/millau.rs
@@ -17,11 +17,36 @@
 //! Millau chain specification for CLI.
 
 use crate::cli::{encode_message::CliEncodeMessage, CliChain};
-use bp_runtime::EncodedOrDecodedCall;
+use bp_runtime::{ChainId, EncodedOrDecodedCall, RIALTO_CHAIN_ID, RIALTO_PARACHAIN_CHAIN_ID};
+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: ChainId,
+		at_target_xcm: xcm::v3::Xcm<()>,
+	) -> anyhow::Result<Vec<u8>> {
+		let target = match target {
+			RIALTO_CHAIN_ID => CustomNetworkId::Rialto.as_network_id(),
+			RIALTO_PARACHAIN_CHAIN_ID => CustomNetworkId::RialtoParachain.as_network_id(),
+			_ => return Err(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..fb3003b30d71fb9b5bd5e597f98908e84936dafd 100644
--- a/bridges/relays/bin-substrate/src/chains/rialto.rs
+++ b/bridges/relays/bin-substrate/src/chains/rialto.rs
@@ -17,11 +17,34 @@
 //! Rialto chain specification for CLI.
 
 use crate::cli::{encode_message::CliEncodeMessage, CliChain};
-use bp_runtime::EncodedOrDecodedCall;
+use bp_runtime::{ChainId, EncodedOrDecodedCall, MILLAU_CHAIN_ID};
+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: ChainId,
+		at_target_xcm: xcm::v3::Xcm<()>,
+	) -> anyhow::Result<Vec<u8>> {
+		let target = match target {
+			MILLAU_CHAIN_ID => CustomNetworkId::Millau.as_network_id(),
+			_ => return Err(anyhow::format_err!("Unsupported target chian: {:?}", 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..f1de10b5c5f31a8a70b62739f576bf24b36c60ea 100644
--- a/bridges/relays/bin-substrate/src/chains/rialto_parachain.rs
+++ b/bridges/relays/bin-substrate/src/chains/rialto_parachain.rs
@@ -17,11 +17,34 @@
 //! Rialto parachain specification for CLI.
 
 use crate::cli::{encode_message::CliEncodeMessage, CliChain};
-use bp_runtime::EncodedOrDecodedCall;
+use bp_runtime::{ChainId, EncodedOrDecodedCall, MILLAU_CHAIN_ID};
+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: ChainId,
+		at_target_xcm: xcm::v3::Xcm<()>,
+	) -> anyhow::Result<Vec<u8>> {
+		let target = match target {
+			MILLAU_CHAIN_ID => CustomNetworkId::Millau.as_network_id(),
+			_ => return Err(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..25231a970bfcb963bb985f7f246ccad09590f94f 100644
--- a/bridges/relays/bin-substrate/src/cli/encode_message.rs
+++ b/bridges/relays/bin-substrate/src/cli/encode_message.rs
@@ -15,11 +15,13 @@
 // along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
 
 use crate::cli::{ExplicitOrMaximal, HexBytes};
-use bp_runtime::EncodedOrDecodedCall;
+use bp_runtime::{ChainId, 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,31 @@ 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))?;
+		let this_location: InteriorMultiLocation = this_network.into();
+
+		let origin = MultiLocation {
+			parents: 0,
+			interior: X1(AccountId32 { network: Some(this_network), id: [0u8; 32] }),
+		};
+		let universal_source = this_location
+			.within_global(origin)
+			.map_err(|e| anyhow::format_err!("Invalid location: {:?}", e))?;
+
+		Ok(universal_source)
+	}
+	/// 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: ChainId,
+		at_target_xcm: xcm::v3::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 +83,42 @@ 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![xcm::v3::Instruction::ClearOrigin; expected_size].into();
+			let at_target_xcm_size =
+				Source::encode_wire_message(Target::ID, at_target_xcm)?.encoded_size();
+			let at_target_xcm_overhead = at_target_xcm_size.saturating_sub(expected_size);
+			let at_target_xcm = vec![
+				xcm::v3::Instruction::ClearOrigin;
+				expected_size.saturating_sub(at_target_xcm_overhead)
+			]
+			.into();
+
+			xcm::VersionedXcm::<()>::V3(
+				vec![ExportMessage {
+					network: destination,
+					destination: destination.into(),
+					xcm: at_target_xcm,
+				}]
+				.into(),
+			)
+			.encode()
 		},
 	})
 }
@@ -123,13 +151,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 +180,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();
 	}