diff --git a/Cargo.lock b/Cargo.lock
index 8a7ba656af1253485ad7eb410e2861ccba9f2b0a..0dda623c14abbacfa0cd86dd311c7adb459c31d0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -896,21 +896,27 @@ dependencies = [
  "cumulus-pallet-xcmp-queue",
  "emulated-integration-tests-common",
  "frame-support",
+ "frame-system",
  "pallet-asset-conversion",
+ "pallet-asset-tx-payment",
  "pallet-assets",
  "pallet-balances",
  "pallet-message-queue",
+ "pallet-transaction-payment",
  "pallet-treasury",
  "pallet-xcm",
  "parachains-common",
  "parity-scale-codec",
  "penpal-runtime",
  "polkadot-runtime-common",
+ "sp-core",
+ "sp-keyring",
  "sp-runtime",
  "staging-xcm",
  "staging-xcm-executor",
  "westend-runtime",
  "westend-system-emulated-network",
+ "xcm-fee-payment-runtime-api",
 ]
 
 [[package]]
@@ -3930,6 +3936,7 @@ dependencies = [
  "sp-trie",
  "sp-version",
  "staging-xcm",
+ "staging-xcm-builder",
  "trie-db",
  "trie-standardmap",
 ]
@@ -5122,6 +5129,19 @@ dependencies = [
  "regex",
 ]
 
+[[package]]
+name = "env_logger"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7"
+dependencies = [
+ "atty",
+ "humantime",
+ "log",
+ "regex",
+ "termcolor",
+]
+
 [[package]]
 name = "env_logger"
 version = "0.10.1"
@@ -12145,6 +12165,7 @@ dependencies = [
  "staging-xcm-builder",
  "staging-xcm-executor",
  "substrate-wasm-builder",
+ "xcm-fee-payment-runtime-api",
 ]
 
 [[package]]
@@ -22651,8 +22672,10 @@ dependencies = [
  "sp-consensus-beefy",
  "sp-core",
  "sp-runtime",
+ "staging-xcm",
  "westend-runtime",
  "westend-runtime-constants",
+ "xcm-fee-payment-runtime-api",
 ]
 
 [[package]]
@@ -23292,14 +23315,24 @@ dependencies = [
 name = "xcm-fee-payment-runtime-api"
 version = "0.1.0"
 dependencies = [
+ "env_logger 0.9.3",
+ "frame-executive",
  "frame-support",
+ "frame-system",
+ "log",
+ "pallet-assets",
+ "pallet-balances",
+ "pallet-xcm",
  "parity-scale-codec",
  "scale-info",
  "sp-api",
+ "sp-io",
  "sp-runtime",
  "sp-std 14.0.0",
  "sp-weights",
  "staging-xcm",
+ "staging-xcm-builder",
+ "staging-xcm-executor",
 ]
 
 [[package]]
diff --git a/bridges/modules/xcm-bridge-hub-router/src/lib.rs b/bridges/modules/xcm-bridge-hub-router/src/lib.rs
index ece72ac8494b763035b7ebfead71e3e280bf4b5c..607394603466e6cb60d20dbefa4aa47580b54c42 100644
--- a/bridges/modules/xcm-bridge-hub-router/src/lib.rs
+++ b/bridges/modules/xcm-bridge-hub-router/src/lib.rs
@@ -37,8 +37,9 @@ use codec::Encode;
 use frame_support::traits::Get;
 use sp_core::H256;
 use sp_runtime::{FixedPointNumber, FixedU128, Saturating};
+use sp_std::vec::Vec;
 use xcm::prelude::*;
-use xcm_builder::{ExporterFor, SovereignPaidRemoteExporter};
+use xcm_builder::{ExporterFor, InspectMessageQueues, SovereignPaidRemoteExporter};
 
 pub use pallet::*;
 pub use weights::WeightInfo;
@@ -95,7 +96,7 @@ pub mod pallet {
 		/// Origin of the sibling bridge hub that is allowed to report bridge status.
 		type BridgeHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
 		/// Actual message sender (`HRMP` or `DMP`) to the sibling bridge hub location.
-		type ToBridgeHubSender: SendXcm;
+		type ToBridgeHubSender: SendXcm + InspectMessageQueues;
 		/// Underlying channel with the sibling bridge hub. It must match the channel, used
 		/// by the `Self::ToBridgeHubSender`.
 		type WithBridgeHubChannel: XcmChannelStatusProvider;
@@ -396,6 +397,12 @@ impl<T: Config<I>, I: 'static> SendXcm for Pallet<T, I> {
 	}
 }
 
+impl<T: Config<I>, I: 'static> InspectMessageQueues for Pallet<T, I> {
+	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
+		ViaBridgeHubExporter::<T, I>::get_messages()
+	}
+}
+
 #[cfg(test)]
 mod tests {
 	use super::*;
@@ -635,4 +642,36 @@ mod tests {
 			);
 		});
 	}
+
+	#[test]
+	fn get_messages_works() {
+		run_test(|| {
+			assert_ok!(send_xcm::<XcmBridgeHubRouter>(
+				(Parent, Parent, GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)).into(),
+				vec![ClearOrigin].into()
+			));
+			assert_eq!(
+				XcmBridgeHubRouter::get_messages(),
+				vec![(
+					VersionedLocation::V4((Parent, Parachain(1002)).into()),
+					vec![VersionedXcm::V4(
+						Xcm::builder()
+							.withdraw_asset((Parent, 1_002_000))
+							.buy_execution((Parent, 1_002_000), Unlimited)
+							.set_appendix(
+								Xcm::builder_unsafe()
+									.deposit_asset(AllCounted(1), (Parent, Parachain(1000)))
+									.build()
+							)
+							.export_message(
+								Kusama,
+								Parachain(1000),
+								Xcm::builder_unsafe().clear_origin().build()
+							)
+							.build()
+					)],
+				),],
+			);
+		});
+	}
 }
diff --git a/bridges/modules/xcm-bridge-hub-router/src/mock.rs b/bridges/modules/xcm-bridge-hub-router/src/mock.rs
index 20c86d1da9a2e315c9465b7e8fe0633401a3a3d4..3e2c1bb369cb72439e53e492cf638a404caa99a3 100644
--- a/bridges/modules/xcm-bridge-hub-router/src/mock.rs
+++ b/bridges/modules/xcm-bridge-hub-router/src/mock.rs
@@ -19,14 +19,16 @@
 use crate as pallet_xcm_bridge_hub_router;
 
 use bp_xcm_bridge_hub_router::XcmChannelStatusProvider;
+use codec::Encode;
 use frame_support::{
 	construct_runtime, derive_impl, parameter_types,
 	traits::{Contains, Equals},
 };
 use frame_system::EnsureRoot;
 use sp_runtime::{traits::ConstU128, BuildStorage};
+use sp_std::cell::RefCell;
 use xcm::prelude::*;
-use xcm_builder::{NetworkExportTable, NetworkExportTableItem};
+use xcm_builder::{InspectMessageQueues, NetworkExportTable, NetworkExportTableItem};
 
 pub type AccountId = u64;
 type Block = frame_system::mocking::MockBlock<TestRuntime>;
@@ -102,23 +104,46 @@ pub struct TestToBridgeHubSender;
 
 impl TestToBridgeHubSender {
 	pub fn is_message_sent() -> bool {
-		frame_support::storage::unhashed::get_or_default(b"TestToBridgeHubSender.Sent")
+		!Self::get_messages().is_empty()
 	}
 }
 
+thread_local! {
+	pub static SENT_XCM: RefCell<Vec<(Location, Xcm<()>)>> = RefCell::new(Vec::new());
+}
+
 impl SendXcm for TestToBridgeHubSender {
-	type Ticket = ();
+	type Ticket = (Location, Xcm<()>);
 
 	fn validate(
-		_destination: &mut Option<Location>,
-		_message: &mut Option<Xcm<()>>,
+		destination: &mut Option<Location>,
+		message: &mut Option<Xcm<()>>,
 	) -> SendResult<Self::Ticket> {
-		Ok(((), (BridgeFeeAsset::get(), HRMP_FEE).into()))
+		let pair = (destination.take().unwrap(), message.take().unwrap());
+		Ok((pair, (BridgeFeeAsset::get(), HRMP_FEE).into()))
 	}
 
-	fn deliver(_ticket: Self::Ticket) -> Result<XcmHash, SendError> {
-		frame_support::storage::unhashed::put(b"TestToBridgeHubSender.Sent", &true);
-		Ok([0u8; 32])
+	fn deliver(pair: Self::Ticket) -> Result<XcmHash, SendError> {
+		let hash = fake_message_hash(&pair.1);
+		SENT_XCM.with(|q| q.borrow_mut().push(pair));
+		Ok(hash)
+	}
+}
+
+impl InspectMessageQueues for TestToBridgeHubSender {
+	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
+		SENT_XCM.with(|q| {
+			(*q.borrow())
+				.clone()
+				.iter()
+				.map(|(location, message)| {
+					(
+						VersionedLocation::V4(location.clone()),
+						vec![VersionedXcm::V4(message.clone())],
+					)
+				})
+				.collect()
+		})
 	}
 }
 
@@ -146,3 +171,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities {
 pub fn run_test<T>(test: impl FnOnce() -> T) -> T {
 	new_test_ext().execute_with(test)
 }
+
+pub(crate) fn fake_message_hash<T>(message: &Xcm<T>) -> XcmHash {
+	message.using_encoded(sp_io::hashing::blake2_256)
+}
diff --git a/cumulus/pallets/parachain-system/Cargo.toml b/cumulus/pallets/parachain-system/Cargo.toml
index cc2e8943caad529ec05d6daa82598dc4c210aeee..57e274db361de98533fd258c554abcff498060b3 100644
--- a/cumulus/pallets/parachain-system/Cargo.toml
+++ b/cumulus/pallets/parachain-system/Cargo.toml
@@ -38,6 +38,7 @@ polkadot-parachain-primitives = { path = "../../../polkadot/parachain", default-
 polkadot-runtime-parachains = { path = "../../../polkadot/runtime/parachains", default-features = false }
 polkadot-runtime-common = { path = "../../../polkadot/runtime/common", default-features = false, optional = true }
 xcm = { package = "staging-xcm", path = "../../../polkadot/xcm", default-features = false }
+xcm-builder = { package = "staging-xcm-builder", path = "../../../polkadot/xcm/xcm-builder", default-features = false }
 
 # Cumulus
 cumulus-pallet-parachain-system-proc-macro = { path = "proc-macro", default-features = false }
@@ -95,6 +96,7 @@ std = [
 	"sp-tracing/std",
 	"sp-trie/std",
 	"trie-db/std",
+	"xcm-builder/std",
 	"xcm/std",
 ]
 
@@ -109,6 +111,7 @@ runtime-benchmarks = [
 	"polkadot-runtime-common/runtime-benchmarks",
 	"polkadot-runtime-parachains/runtime-benchmarks",
 	"sp-runtime/runtime-benchmarks",
+	"xcm-builder/runtime-benchmarks",
 ]
 
 try-runtime = [
diff --git a/cumulus/pallets/parachain-system/src/lib.rs b/cumulus/pallets/parachain-system/src/lib.rs
index 54a1def59600dff5dd8b1484691dcb18fff5ce63..c8e7d1bb30f7166d50ab9aed8d1dfd0526e8f36e 100644
--- a/cumulus/pallets/parachain-system/src/lib.rs
+++ b/cumulus/pallets/parachain-system/src/lib.rs
@@ -55,7 +55,8 @@ use sp_runtime::{
 	BoundedSlice, FixedU128, RuntimeDebug, Saturating,
 };
 use sp_std::{cmp, collections::btree_map::BTreeMap, prelude::*};
-use xcm::latest::XcmHash;
+use xcm::{latest::XcmHash, VersionedLocation, VersionedXcm};
+use xcm_builder::InspectMessageQueues;
 
 mod benchmarking;
 pub mod migration;
@@ -1608,6 +1609,19 @@ impl<T: Config> UpwardMessageSender for Pallet<T> {
 	}
 }
 
+impl<T: Config> InspectMessageQueues for Pallet<T> {
+	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
+		use xcm::prelude::*;
+
+		let messages: Vec<VersionedXcm<()>> = PendingUpwardMessages::<T>::get()
+			.iter()
+			.map(|encoded_message| VersionedXcm::<()>::decode(&mut &encoded_message[..]).unwrap())
+			.collect();
+
+		vec![(VersionedLocation::V4(Parent.into()), messages)]
+	}
+}
+
 #[cfg(feature = "runtime-benchmarks")]
 impl<T: Config> polkadot_runtime_common::xcm_sender::EnsureForParachain for Pallet<T> {
 	fn ensure(para_id: ParaId) {
diff --git a/cumulus/pallets/xcmp-queue/Cargo.toml b/cumulus/pallets/xcmp-queue/Cargo.toml
index ab196c6d3ec6a089d8099d6ae893631ac513e9d7..e3530ef7bf0e1e093ddb85e639a7a3ff757ad2da 100644
--- a/cumulus/pallets/xcmp-queue/Cargo.toml
+++ b/cumulus/pallets/xcmp-queue/Cargo.toml
@@ -28,6 +28,7 @@ polkadot-runtime-common = { path = "../../../polkadot/runtime/common", default-f
 polkadot-runtime-parachains = { path = "../../../polkadot/runtime/parachains", default-features = false }
 xcm = { package = "staging-xcm", path = "../../../polkadot/xcm", default-features = false }
 xcm-executor = { package = "staging-xcm-executor", path = "../../../polkadot/xcm/xcm-executor", default-features = false }
+xcm-builder = { package = "staging-xcm-builder", path = "../../../polkadot/xcm/xcm-builder", default-features = false }
 
 # Cumulus
 cumulus-primitives-core = { path = "../../primitives/core", default-features = false }
@@ -46,9 +47,6 @@ sp-core = { path = "../../../substrate/primitives/core" }
 pallet-balances = { path = "../../../substrate/frame/balances" }
 frame-support = { path = "../../../substrate/frame/support", features = ["experimental"] }
 
-# Polkadot
-xcm-builder = { package = "staging-xcm-builder", path = "../../../polkadot/xcm/xcm-builder" }
-
 # Cumulus
 cumulus-pallet-parachain-system = { path = "../parachain-system", features = ["parameterized-consensus-hook"] }
 
@@ -71,6 +69,7 @@ std = [
 	"sp-io/std",
 	"sp-runtime/std",
 	"sp-std/std",
+	"xcm-builder/std",
 	"xcm-executor/std",
 	"xcm/std",
 ]
diff --git a/cumulus/pallets/xcmp-queue/src/lib.rs b/cumulus/pallets/xcmp-queue/src/lib.rs
index 7de2fd8094213aaa0f8b8ef0dbb4539556b48465..cc785b66150e52731cdb03d2c41aaad0058d3ef9 100644
--- a/cumulus/pallets/xcmp-queue/src/lib.rs
+++ b/cumulus/pallets/xcmp-queue/src/lib.rs
@@ -70,7 +70,8 @@ use scale_info::TypeInfo;
 use sp_core::MAX_POSSIBLE_ALLOCATION;
 use sp_runtime::{FixedU128, RuntimeDebug, Saturating};
 use sp_std::prelude::*;
-use xcm::{latest::prelude::*, VersionedXcm, WrapVersion, MAX_XCM_DECODE_DEPTH};
+use xcm::{latest::prelude::*, VersionedLocation, VersionedXcm, WrapVersion, MAX_XCM_DECODE_DEPTH};
+use xcm_builder::InspectMessageQueues;
 use xcm_executor::traits::ConvertOrigin;
 
 pub use pallet::*;
@@ -947,6 +948,38 @@ impl<T: Config> SendXcm for Pallet<T> {
 	}
 }
 
+impl<T: Config> InspectMessageQueues for Pallet<T> {
+	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
+		use xcm::prelude::*;
+
+		OutboundXcmpMessages::<T>::iter()
+			.map(|(para_id, _, messages)| {
+				let mut data = &messages[..];
+				let decoded_format =
+					XcmpMessageFormat::decode_with_depth_limit(MAX_XCM_DECODE_DEPTH, &mut data)
+						.unwrap();
+				if decoded_format != XcmpMessageFormat::ConcatenatedVersionedXcm {
+					panic!("Unexpected format.")
+				}
+				let mut decoded_messages = Vec::new();
+				while !data.is_empty() {
+					let decoded_message = VersionedXcm::<()>::decode_with_depth_limit(
+						MAX_XCM_DECODE_DEPTH,
+						&mut data,
+					)
+					.unwrap();
+					decoded_messages.push(decoded_message);
+				}
+
+				(
+					VersionedLocation::V4((Parent, Parachain(para_id.into())).into()),
+					decoded_messages,
+				)
+			})
+			.collect()
+	}
+}
+
 impl<T: Config> FeeTracker for Pallet<T> {
 	type Id = ParaId;
 
diff --git a/cumulus/pallets/xcmp-queue/src/mock.rs b/cumulus/pallets/xcmp-queue/src/mock.rs
index 9d9a723cf8b538b604c45ff5b2bd366cd6f92c2d..e258576aa3f6dd0342f712498654ef102fb6f321 100644
--- a/cumulus/pallets/xcmp-queue/src/mock.rs
+++ b/cumulus/pallets/xcmp-queue/src/mock.rs
@@ -178,6 +178,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = ();
 }
 
 pub type XcmRouter = (
diff --git a/cumulus/pallets/xcmp-queue/src/tests.rs b/cumulus/pallets/xcmp-queue/src/tests.rs
index 0b41095828f2f93810a2855d175ecd60a12853d7..f48e9eec3ac05b85794a2f18fc279566ad613272 100644
--- a/cumulus/pallets/xcmp-queue/src/tests.rs
+++ b/cumulus/pallets/xcmp-queue/src/tests.rs
@@ -844,3 +844,43 @@ fn verify_fee_factor_increase_and_decrease() {
 		assert!(DeliveryFeeFactor::<Test>::get(sibling_para_id) < FixedU128::from_float(1.63));
 	});
 }
+
+#[test]
+fn get_messages_works() {
+	new_test_ext().execute_with(|| {
+		use xcm_builder::InspectMessageQueues;
+		let sibling_para_id = ParaId::from(2001);
+		ParachainSystem::open_outbound_hrmp_channel_for_benchmarks_or_tests(sibling_para_id);
+		let destination: Location = (Parent, Parachain(sibling_para_id.into())).into();
+		let other_sibling_para_id = ParaId::from(2002);
+		let other_destination: Location = (Parent, Parachain(other_sibling_para_id.into())).into();
+		let message = Xcm(vec![ClearOrigin]);
+		assert_ok!(send_xcm::<XcmpQueue>(destination.clone(), message.clone()));
+		assert_ok!(send_xcm::<XcmpQueue>(destination.clone(), message.clone()));
+		assert_ok!(send_xcm::<XcmpQueue>(destination.clone(), message.clone()));
+		ParachainSystem::open_outbound_hrmp_channel_for_benchmarks_or_tests(other_sibling_para_id);
+		assert_ok!(send_xcm::<XcmpQueue>(other_destination.clone(), message.clone()));
+		assert_ok!(send_xcm::<XcmpQueue>(other_destination.clone(), message));
+		let queued_messages = XcmpQueue::get_messages();
+		assert_eq!(
+			queued_messages,
+			vec![
+				(
+					VersionedLocation::V4(other_destination),
+					vec![
+						VersionedXcm::V4(Xcm(vec![ClearOrigin])),
+						VersionedXcm::V4(Xcm(vec![ClearOrigin])),
+					],
+				),
+				(
+					VersionedLocation::V4(destination),
+					vec![
+						VersionedXcm::V4(Xcm(vec![ClearOrigin])),
+						VersionedXcm::V4(Xcm(vec![ClearOrigin])),
+						VersionedXcm::V4(Xcm(vec![ClearOrigin])),
+					],
+				),
+			],
+		);
+	});
+}
diff --git a/cumulus/parachains/integration-tests/emulated/chains/relays/westend/Cargo.toml b/cumulus/parachains/integration-tests/emulated/chains/relays/westend/Cargo.toml
index 20aedb50e6a18ef1112ab23b2a6d05780b6b443f..e4688a1c9f022dd9aafd821ee39bfc5906a7b2a0 100644
--- a/cumulus/parachains/integration-tests/emulated/chains/relays/westend/Cargo.toml
+++ b/cumulus/parachains/integration-tests/emulated/chains/relays/westend/Cargo.toml
@@ -25,6 +25,8 @@ pallet-staking = { path = "../../../../../../../substrate/frame/staking", defaul
 polkadot-primitives = { path = "../../../../../../../polkadot/primitives", default-features = false }
 westend-runtime-constants = { path = "../../../../../../../polkadot/runtime/westend/constants", default-features = false }
 westend-runtime = { path = "../../../../../../../polkadot/runtime/westend" }
+xcm = { package = "staging-xcm", path = "../../../../../../../polkadot/xcm", default-features = false }
+xcm-fee-payment-runtime-api = { path = "../../../../../../../polkadot/xcm/xcm-fee-payment-runtime-api", default-features = false }
 
 # Cumulus
 parachains-common = { path = "../../../../../common" }
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml
index 00f4308324a96734afac879cafd1e51300407a26..0a2b0f6d45ee0480fe91392576068b1cafcb6cdf 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/Cargo.toml
@@ -16,18 +16,24 @@ assert_matches = "1.5.0"
 
 # Substrate
 sp-runtime = { path = "../../../../../../../substrate/primitives/runtime", default-features = false }
+sp-keyring = { path = "../../../../../../../substrate/primitives/keyring", default-features = false }
+sp-core = { path = "../../../../../../../substrate/primitives/core", default-features = false }
 frame-support = { path = "../../../../../../../substrate/frame/support", default-features = false }
+frame-system = { path = "../../../../../../../substrate/frame/system", default-features = false }
 pallet-balances = { path = "../../../../../../../substrate/frame/balances", default-features = false }
 pallet-assets = { path = "../../../../../../../substrate/frame/assets", default-features = false }
 pallet-asset-conversion = { path = "../../../../../../../substrate/frame/asset-conversion", default-features = false }
 pallet-treasury = { path = "../../../../../../../substrate/frame/treasury", default-features = false }
 pallet-message-queue = { path = "../../../../../../../substrate/frame/message-queue", default-features = false }
+pallet-transaction-payment = { path = "../../../../../../../substrate/frame/transaction-payment", default-features = false }
+pallet-asset-tx-payment = { path = "../../../../../../../substrate/frame/transaction-payment/asset-tx-payment", default-features = false }
 
 # Polkadot
 polkadot-runtime-common = { path = "../../../../../../../polkadot/runtime/common" }
 xcm = { package = "staging-xcm", path = "../../../../../../../polkadot/xcm", default-features = false }
 xcm-executor = { package = "staging-xcm-executor", path = "../../../../../../../polkadot/xcm/xcm-executor", default-features = false }
 pallet-xcm = { path = "../../../../../../../polkadot/xcm/pallet-xcm", default-features = false }
+xcm-fee-payment-runtime-api = { path = "../../../../../../../polkadot/xcm/xcm-fee-payment-runtime-api", default-features = false }
 westend-runtime = { path = "../../../../../../../polkadot/runtime/westend" }
 
 # Cumulus
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs
index bf013697b4c750c8a9f3da3732e97386ba1cae74..61eb70524fc9ae2a23e5061400c57ca719bae4e1 100644
--- a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/mod.rs
@@ -21,3 +21,4 @@ mod set_xcm_versions;
 mod swap;
 mod teleport;
 mod treasury;
+mod xcm_fee_estimation;
diff --git a/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs
new file mode 100644
index 0000000000000000000000000000000000000000..aeec9b44dab4ce7648e609e13136c381a5a50695
--- /dev/null
+++ b/cumulus/parachains/integration-tests/emulated/tests/assets/asset-hub-westend/src/tests/xcm_fee_estimation.rs
@@ -0,0 +1,370 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// 	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Tests to ensure correct XCM fee estimation for cross-chain asset transfers.
+
+use crate::imports::*;
+
+use sp_keyring::AccountKeyring::Alice;
+use sp_runtime::{generic, MultiSignature};
+use xcm_fee_payment_runtime_api::{
+	dry_run::runtime_decl_for_xcm_dry_run_api::XcmDryRunApiV1,
+	fees::runtime_decl_for_xcm_payment_api::XcmPaymentApiV1,
+};
+
+/// We are able to dry-run and estimate the fees for a teleport between relay and system para.
+/// Scenario: Alice on Westend relay chain wants to teleport WND to Asset Hub.
+/// We want to know the fees using the `XcmDryRunApi` and `XcmPaymentApi`.
+#[test]
+fn teleport_relay_system_para_works() {
+	let destination: Location = Parachain(1000).into(); // Asset Hub.
+	let beneficiary_id = AssetHubWestendReceiver::get();
+	let beneficiary: Location = AccountId32 { id: beneficiary_id.clone().into(), network: None } // Test doesn't allow specifying a network here.
+		.into(); // Beneficiary in Asset Hub.
+	let teleport_amount = 1_000_000_000_000; // One WND (12 decimals).
+	let assets: Assets = vec![(Here, teleport_amount).into()].into();
+
+	// We get them from the Westend closure.
+	let mut delivery_fees_amount = 0;
+	let mut remote_message = VersionedXcm::V4(Xcm(Vec::new()));
+	<Westend as TestExt>::new_ext().execute_with(|| {
+		type Runtime = <Westend as Chain>::Runtime;
+		type RuntimeCall = <Westend as Chain>::RuntimeCall;
+
+		let call = RuntimeCall::XcmPallet(pallet_xcm::Call::transfer_assets {
+			dest: Box::new(VersionedLocation::V4(destination.clone())),
+			beneficiary: Box::new(VersionedLocation::V4(beneficiary)),
+			assets: Box::new(VersionedAssets::V4(assets)),
+			fee_asset_item: 0,
+			weight_limit: Unlimited,
+		});
+		let sender = Alice; // Is the same as `WestendSender`.
+		let extrinsic = construct_extrinsic_westend(sender, call);
+		let result = Runtime::dry_run_extrinsic(extrinsic).unwrap();
+		assert_eq!(result.forwarded_xcms.len(), 1);
+		let (destination_to_query, messages_to_query) = &result.forwarded_xcms[0];
+		assert_eq!(messages_to_query.len(), 1);
+		remote_message = messages_to_query[0].clone();
+		let delivery_fees =
+			Runtime::query_delivery_fees(destination_to_query.clone(), remote_message.clone())
+				.unwrap();
+		delivery_fees_amount = get_amount_from_versioned_assets(delivery_fees);
+	});
+
+	// This is set in the AssetHubWestend closure.
+	let mut remote_execution_fees = 0;
+	<AssetHubWestend as TestExt>::execute_with(|| {
+		type Runtime = <AssetHubWestend as Chain>::Runtime;
+
+		let weight = Runtime::query_xcm_weight(remote_message.clone()).unwrap();
+		remote_execution_fees =
+			Runtime::query_weight_to_asset_fee(weight, VersionedAssetId::V4(Parent.into()))
+				.unwrap();
+	});
+
+	let test_args = TestContext {
+		sender: WestendSender::get(),             // Alice.
+		receiver: AssetHubWestendReceiver::get(), // Bob in Asset Hub.
+		args: TestArgs::new_relay(destination, beneficiary_id, teleport_amount),
+	};
+	let mut test = RelayToSystemParaTest::new(test_args);
+
+	let sender_balance_before = test.sender.balance;
+	let receiver_balance_before = test.receiver.balance;
+	assert_eq!(sender_balance_before, 1_000_000_000_000_000_000);
+	assert_eq!(receiver_balance_before, 4_096_000_000_000);
+
+	test.set_dispatchable::<Westend>(transfer_assets);
+	test.assert();
+
+	let sender_balance_after = test.sender.balance;
+	let receiver_balance_after = test.receiver.balance;
+
+	// We now know the exact fees.
+	assert_eq!(
+		sender_balance_after,
+		sender_balance_before - delivery_fees_amount - teleport_amount
+	);
+	assert_eq!(
+		receiver_balance_after,
+		receiver_balance_before + teleport_amount - remote_execution_fees
+	);
+}
+
+/// We are able to dry-run and estimate the fees for a multi-hop XCM journey.
+/// Scenario: Alice on PenpalA has some WND and wants to send them to PenpalB.
+/// We want to know the fees using the `XcmDryRunApi` and `XcmPaymentApi`.
+#[test]
+fn multi_hop_works() {
+	let destination = PenpalA::sibling_location_of(PenpalB::para_id());
+	let sender = PenpalASender::get();
+	let amount_to_send = 1_000_000_000_000; // One WND (12 decimals).
+	let asset_owner = PenpalAssetOwner::get();
+	let assets: Assets = (Parent, amount_to_send).into();
+	let relay_native_asset_location = RelayLocation::get();
+	let sender_as_seen_by_relay = Westend::child_location_of(PenpalA::para_id());
+	let sov_of_sender_on_relay = Westend::sovereign_account_id_of(sender_as_seen_by_relay.clone());
+
+	// fund Parachain's sender account
+	PenpalA::mint_foreign_asset(
+		<PenpalA as Chain>::RuntimeOrigin::signed(asset_owner.clone()),
+		relay_native_asset_location.clone(),
+		sender.clone(),
+		amount_to_send * 2,
+	);
+
+	// fund the Parachain Origin's SA on Relay Chain with the native tokens held in reserve
+	Westend::fund_accounts(vec![(sov_of_sender_on_relay.clone().into(), amount_to_send * 2)]);
+
+	// Init values for Parachain Destination
+	let beneficiary_id = PenpalBReceiver::get();
+	let beneficiary: Location = AccountId32 {
+		id: beneficiary_id.clone().into(),
+		network: None, // Test doesn't allow specifying a network here.
+	}
+	.into();
+
+	// We get them from the PenpalA closure.
+	let mut delivery_fees_amount = 0;
+	let mut remote_message = VersionedXcm::V4(Xcm(Vec::new()));
+	<PenpalA as TestExt>::execute_with(|| {
+		type Runtime = <PenpalA as Chain>::Runtime;
+		type RuntimeCall = <PenpalA as Chain>::RuntimeCall;
+
+		let call = RuntimeCall::PolkadotXcm(pallet_xcm::Call::transfer_assets {
+			dest: Box::new(VersionedLocation::V4(destination.clone())),
+			beneficiary: Box::new(VersionedLocation::V4(beneficiary)),
+			assets: Box::new(VersionedAssets::V4(assets.clone())),
+			fee_asset_item: 0,
+			weight_limit: Unlimited,
+		});
+		let sender = Alice; // Same as `PenpalASender`.
+		let extrinsic = construct_extrinsic_penpal(sender, call);
+		let result = Runtime::dry_run_extrinsic(extrinsic).unwrap();
+		assert_eq!(result.forwarded_xcms.len(), 1);
+		let (destination_to_query, messages_to_query) = &result.forwarded_xcms[0];
+		assert_eq!(messages_to_query.len(), 1);
+		remote_message = messages_to_query[0].clone();
+		let delivery_fees =
+			Runtime::query_delivery_fees(destination_to_query.clone(), remote_message.clone())
+				.unwrap();
+		delivery_fees_amount = get_amount_from_versioned_assets(delivery_fees);
+	});
+
+	// This is set in the Westend closure.
+	let mut intermediate_execution_fees = 0;
+	let mut intermediate_delivery_fees_amount = 0;
+	let mut intermediate_remote_message = VersionedXcm::V4(Xcm::<()>(Vec::new()));
+	<Westend as TestExt>::execute_with(|| {
+		type Runtime = <Westend as Chain>::Runtime;
+		type RuntimeCall = <Westend as Chain>::RuntimeCall;
+
+		// First we get the execution fees.
+		let weight = Runtime::query_xcm_weight(remote_message.clone()).unwrap();
+		intermediate_execution_fees =
+			Runtime::query_weight_to_asset_fee(weight, VersionedAssetId::V4(Here.into())).unwrap();
+
+		// We have to do this to turn `VersionedXcm<()>` into `VersionedXcm<RuntimeCall>`.
+		let xcm_program =
+			VersionedXcm::V4(Xcm::<RuntimeCall>::from(remote_message.clone().try_into().unwrap()));
+
+		// Now we get the delivery fees to the final destination.
+		let result =
+			Runtime::dry_run_xcm(sender_as_seen_by_relay.clone().into(), xcm_program).unwrap();
+		let (destination_to_query, messages_to_query) = &result.forwarded_xcms[0];
+		// There's actually two messages here.
+		// One created when the message we sent from PenpalA arrived and was executed.
+		// The second one when we dry-run the xcm.
+		// We could've gotten the message from the queue without having to dry-run, but
+		// offchain applications would have to dry-run, so we do it here as well.
+		intermediate_remote_message = messages_to_query[0].clone();
+		let delivery_fees = Runtime::query_delivery_fees(
+			destination_to_query.clone(),
+			intermediate_remote_message.clone(),
+		)
+		.unwrap();
+		intermediate_delivery_fees_amount = get_amount_from_versioned_assets(delivery_fees);
+	});
+
+	// Get the final execution fees in the destination.
+	let mut final_execution_fees = 0;
+	<PenpalB as TestExt>::execute_with(|| {
+		type Runtime = <PenpalB as Chain>::Runtime;
+
+		let weight = Runtime::query_xcm_weight(intermediate_remote_message.clone()).unwrap();
+		final_execution_fees =
+			Runtime::query_weight_to_asset_fee(weight, VersionedAssetId::V4(Parent.into()))
+				.unwrap();
+	});
+
+	// Dry-running is done.
+	PenpalA::reset_ext();
+	Westend::reset_ext();
+	PenpalB::reset_ext();
+
+	// Fund accounts again.
+	PenpalA::mint_foreign_asset(
+		<PenpalA as Chain>::RuntimeOrigin::signed(asset_owner),
+		relay_native_asset_location.clone(),
+		sender.clone(),
+		amount_to_send * 2,
+	);
+	Westend::fund_accounts(vec![(sov_of_sender_on_relay.into(), amount_to_send * 2)]);
+
+	// Actually run the extrinsic.
+	let test_args = TestContext {
+		sender: PenpalASender::get(),     // Alice.
+		receiver: PenpalBReceiver::get(), // Bob in PenpalB.
+		args: TestArgs::new_para(
+			destination,
+			beneficiary_id.clone(),
+			amount_to_send,
+			assets,
+			None,
+			0,
+		),
+	};
+	let mut test = ParaToParaThroughRelayTest::new(test_args);
+
+	let sender_assets_before = PenpalA::execute_with(|| {
+		type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(relay_native_asset_location.clone(), &sender)
+	});
+	let receiver_assets_before = PenpalB::execute_with(|| {
+		type ForeignAssets = <PenpalB as PenpalBPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(relay_native_asset_location.clone(), &beneficiary_id)
+	});
+
+	test.set_dispatchable::<PenpalA>(transfer_assets_para_to_para);
+	test.assert();
+
+	let sender_assets_after = PenpalA::execute_with(|| {
+		type ForeignAssets = <PenpalA as PenpalAPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(relay_native_asset_location.clone(), &sender)
+	});
+	let receiver_assets_after = PenpalB::execute_with(|| {
+		type ForeignAssets = <PenpalB as PenpalBPallet>::ForeignAssets;
+		<ForeignAssets as Inspect<_>>::balance(relay_native_asset_location, &beneficiary_id)
+	});
+
+	// We know the exact fees on every hop.
+	assert_eq!(
+		sender_assets_after,
+		sender_assets_before - amount_to_send - delivery_fees_amount /* This is charged directly
+		                                                              * from the sender's
+		                                                              * account. */
+	);
+	assert_eq!(
+		receiver_assets_after,
+		receiver_assets_before + amount_to_send -
+			intermediate_execution_fees -
+			intermediate_delivery_fees_amount -
+			final_execution_fees
+	);
+}
+
+fn get_amount_from_versioned_assets(assets: VersionedAssets) -> u128 {
+	let latest_assets: Assets = assets.try_into().unwrap();
+	let Fungible(amount) = latest_assets.inner()[0].fun else {
+		unreachable!("asset is fungible");
+	};
+	amount
+}
+
+fn transfer_assets(test: RelayToSystemParaTest) -> DispatchResult {
+	<Westend as WestendPallet>::XcmPallet::transfer_assets(
+		test.signed_origin,
+		bx!(test.args.dest.into()),
+		bx!(test.args.beneficiary.into()),
+		bx!(test.args.assets.into()),
+		test.args.fee_asset_item,
+		test.args.weight_limit,
+	)
+}
+
+fn transfer_assets_para_to_para(test: ParaToParaThroughRelayTest) -> DispatchResult {
+	<PenpalA as PenpalAPallet>::PolkadotXcm::transfer_assets(
+		test.signed_origin,
+		bx!(test.args.dest.into()),
+		bx!(test.args.beneficiary.into()),
+		bx!(test.args.assets.into()),
+		test.args.fee_asset_item,
+		test.args.weight_limit,
+	)
+}
+
+// Constructs the SignedExtra component of an extrinsic for the Westend runtime.
+fn construct_extrinsic_westend(
+	sender: sp_keyring::AccountKeyring,
+	call: westend_runtime::RuntimeCall,
+) -> westend_runtime::UncheckedExtrinsic {
+	type Runtime = <Westend as Chain>::Runtime;
+	let account_id = <Runtime as frame_system::Config>::AccountId::from(sender.public());
+	let tip = 0;
+	let extra: westend_runtime::SignedExtra = (
+		frame_system::CheckNonZeroSender::<Runtime>::new(),
+		frame_system::CheckSpecVersion::<Runtime>::new(),
+		frame_system::CheckTxVersion::<Runtime>::new(),
+		frame_system::CheckGenesis::<Runtime>::new(),
+		frame_system::CheckMortality::<Runtime>::from(sp_runtime::generic::Era::immortal()),
+		frame_system::CheckNonce::<Runtime>::from(
+			frame_system::Pallet::<Runtime>::account(&account_id).nonce,
+		),
+		frame_system::CheckWeight::<Runtime>::new(),
+		pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(tip),
+	);
+	let raw_payload = westend_runtime::SignedPayload::new(call, extra).unwrap();
+	let signature = raw_payload.using_encoded(|payload| sender.sign(payload));
+	let (call, extra, _) = raw_payload.deconstruct();
+	westend_runtime::UncheckedExtrinsic::new_signed(
+		call,
+		account_id.into(),
+		MultiSignature::Sr25519(signature),
+		extra,
+	)
+}
+
+// Constructs the SignedExtra component of an extrinsic for the Westend runtime.
+fn construct_extrinsic_penpal(
+	sender: sp_keyring::AccountKeyring,
+	call: penpal_runtime::RuntimeCall,
+) -> penpal_runtime::UncheckedExtrinsic {
+	type Runtime = <PenpalA as Chain>::Runtime;
+	let account_id = <Runtime as frame_system::Config>::AccountId::from(sender.public());
+	let tip = 0;
+	let extra: penpal_runtime::SignedExtra = (
+		frame_system::CheckNonZeroSender::<Runtime>::new(),
+		frame_system::CheckSpecVersion::<Runtime>::new(),
+		frame_system::CheckTxVersion::<Runtime>::new(),
+		frame_system::CheckGenesis::<Runtime>::new(),
+		frame_system::CheckEra::<Runtime>::from(generic::Era::immortal()),
+		frame_system::CheckNonce::<Runtime>::from(
+			frame_system::Pallet::<Runtime>::account(&account_id).nonce,
+		),
+		frame_system::CheckWeight::<Runtime>::new(),
+		pallet_asset_tx_payment::ChargeAssetTxPayment::<Runtime>::from(tip, None),
+	);
+	type SignedPayload =
+		generic::SignedPayload<penpal_runtime::RuntimeCall, penpal_runtime::SignedExtra>;
+	let raw_payload = SignedPayload::new(call, extra).unwrap();
+	let signature = raw_payload.using_encoded(|payload| sender.sign(payload));
+	let (call, extra, _) = raw_payload.deconstruct();
+	penpal_runtime::UncheckedExtrinsic::new_signed(
+		call,
+		account_id.into(),
+		MultiSignature::Sr25519(signature),
+		extra,
+	)
+}
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml
index 64abedbaac789060aaeada1a7bf236b77017301d..888193c5c6ea7e02e879428b920cd1ed70c15e54 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml
@@ -138,6 +138,7 @@ runtime-benchmarks = [
 	"sp-runtime/runtime-benchmarks",
 	"xcm-builder/runtime-benchmarks",
 	"xcm-executor/runtime-benchmarks",
+	"xcm-fee-payment-runtime-api/runtime-benchmarks",
 ]
 try-runtime = [
 	"cumulus-pallet-aura-ext/try-runtime",
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
index 383751578e57e47ff50ebd25907203dd4c9412f0..f81a107fae0537095ece60c58592ca5e2b7068f7 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
@@ -100,7 +100,10 @@ use xcm::{
 	latest::prelude::{AssetId, BodyId},
 	IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm,
 };
-use xcm_fee_payment_runtime_api::Error as XcmPaymentApiError;
+use xcm_fee_payment_runtime_api::{
+	dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects},
+	fees::Error as XcmPaymentApiError,
+};
 
 use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight};
 
@@ -1281,7 +1284,7 @@ impl_runtime_apis! {
 		}
 	}
 
-	impl xcm_fee_payment_runtime_api::XcmPaymentApi<Block> for Runtime {
+	impl xcm_fee_payment_runtime_api::fees::XcmPaymentApi<Block> for Runtime {
 		fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
 			let acceptable = vec![
 				// native token
@@ -1320,6 +1323,70 @@ impl_runtime_apis! {
 		}
 	}
 
+	impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi<Block, RuntimeCall, RuntimeEvent> for Runtime {
+		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			use xcm_builder::InspectMessageQueues;
+			use xcm_executor::RecordXcm;
+			use xcm::prelude::*;
+
+			pallet_xcm::Pallet::<Runtime>::set_record_xcm(true);
+			let result = Executive::apply_extrinsic(extrinsic).map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_extrinsic",
+					"Applying extrinsic failed with error {:?}",
+					error,
+				);
+				XcmDryRunApiError::InvalidExtrinsic
+			})?;
+			let local_xcm = pallet_xcm::Pallet::<Runtime>::recorded_xcm();
+			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
+			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
+			Ok(ExtrinsicDryRunEffects {
+				local_xcm: local_xcm.map(VersionedXcm::<()>::V4),
+				forwarded_xcms,
+				emitted_events: events,
+				execution_result: result,
+			})
+		}
+
+		fn dry_run_xcm(origin_location: VersionedLocation, program: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			use xcm_builder::InspectMessageQueues;
+			use xcm::prelude::*;
+
+			let origin_location: Location = origin_location.try_into().map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					"Location version conversion failed with error: {:?}",
+					error,
+				);
+				XcmDryRunApiError::VersionedConversionFailed
+			})?;
+			let program: Xcm<RuntimeCall> = program.try_into().map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					"Xcm version conversion failed with error {:?}",
+					error,
+				);
+				XcmDryRunApiError::VersionedConversionFailed
+			})?;
+			let mut hash = program.using_encoded(sp_core::hashing::blake2_256);
+			let result = xcm_executor::XcmExecutor::<xcm_config::XcmConfig>::prepare_and_execute(
+				origin_location,
+				program,
+				&mut hash,
+				Weight::MAX, // Max limit available for execution.
+				Weight::zero(),
+			);
+			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
+			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
+			Ok(XcmDryRunEffects {
+				forwarded_xcms,
+				emitted_events: events,
+				execution_result: result,
+			})
+		}
+	}
+
 	impl cumulus_primitives_core::CollectCollationInfo<Block> for Runtime {
 		fn collect_collation_info(header: &<Block as BlockT>::Header) -> cumulus_primitives_core::CollationInfo {
 			ParachainSystem::collect_collation_info(header)
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs
index a73c1cc33ea0b958bf165fa54cd554548562f099..664d2b9c9dd593536404f02d895a414e6c232bfa 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/xcm_config.rs
@@ -424,6 +424,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 /// Converts a local signed origin into an XCM location.
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml
index 3ba53eb3f9370c8bb56b364a9980145dd3a679a9..bacc9c1b7c29eba47f0ddc1140771407e7d1ab3c 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml
@@ -135,6 +135,7 @@ runtime-benchmarks = [
 	"sp-runtime/runtime-benchmarks",
 	"xcm-builder/runtime-benchmarks",
 	"xcm-executor/runtime-benchmarks",
+	"xcm-fee-payment-runtime-api/runtime-benchmarks",
 ]
 try-runtime = [
 	"cumulus-pallet-aura-ext/try-runtime",
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
index e96ba3d962d87c7068dfbc75f8084dcdee4c6ee4..b5c3ed5053c4b77e1ecf0adeb8c99c5beecb81e0 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
@@ -85,17 +85,24 @@ pub use sp_runtime::BuildStorage;
 
 use assets_common::{foreign_creators::ForeignCreators, matching::FromSiblingParachain};
 use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate};
+use xcm::{
+	prelude::{VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm},
+	IntoVersion,
+};
+
+// We exclude `Assets` since it's the name of a pallet
+use xcm::latest::prelude::AssetId;
 
 #[cfg(feature = "runtime-benchmarks")]
 use xcm::latest::prelude::{
 	Asset, Fungible, Here, InteriorLocation, Junction, Junction::*, Location, NetworkId,
 	NonFungible, Parent, ParentThen, Response, XCM_VERSION,
 };
-use xcm::{
-	latest::prelude::AssetId, IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation,
-	VersionedXcm,
+
+use xcm_fee_payment_runtime_api::{
+	dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects},
+	fees::Error as XcmPaymentApiError,
 };
-use xcm_fee_payment_runtime_api::Error as XcmPaymentApiError;
 
 use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight};
 
@@ -1311,6 +1318,109 @@ impl_runtime_apis! {
 		}
 	}
 
+	impl xcm_fee_payment_runtime_api::fees::XcmPaymentApi<Block> for Runtime {
+		fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
+			let acceptable = vec![
+				// native token
+				VersionedAssetId::from(AssetId(xcm_config::WestendLocation::get()))
+			];
+
+			Ok(acceptable
+				.into_iter()
+				.filter_map(|asset| asset.into_version(xcm_version).ok())
+				.collect())
+		}
+
+		fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result<u128, XcmPaymentApiError> {
+			match asset.try_as::<AssetId>() {
+				Ok(asset_id) if asset_id.0 == xcm_config::WestendLocation::get() => {
+					// for native token
+					Ok(WeightToFee::weight_to_fee(&weight))
+				},
+				Ok(asset_id) => {
+					log::trace!(target: "xcm::xcm_fee_payment_runtime_api", "query_weight_to_asset_fee - unhandled asset_id: {asset_id:?}!");
+					Err(XcmPaymentApiError::AssetNotFound)
+				},
+				Err(_) => {
+					log::trace!(target: "xcm::xcm_fee_payment_runtime_api", "query_weight_to_asset_fee - failed to convert asset: {asset:?}!");
+					Err(XcmPaymentApiError::VersionedConversionFailed)
+				}
+			}
+		}
+
+		fn query_xcm_weight(message: VersionedXcm<()>) -> Result<Weight, XcmPaymentApiError> {
+			PolkadotXcm::query_xcm_weight(message)
+		}
+
+		fn query_delivery_fees(destination: VersionedLocation, message: VersionedXcm<()>) -> Result<VersionedAssets, XcmPaymentApiError> {
+			PolkadotXcm::query_delivery_fees(destination, message)
+		}
+	}
+
+	impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi<Block, RuntimeCall, RuntimeEvent> for Runtime {
+		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			use xcm_builder::InspectMessageQueues;
+			use xcm_executor::RecordXcm;
+			use xcm::prelude::*;
+
+			pallet_xcm::Pallet::<Runtime>::set_record_xcm(true);
+			let result = Executive::apply_extrinsic(extrinsic).map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_extrinsic",
+					"Applying extrinsic failed with error {:?}",
+					error,
+				);
+				XcmDryRunApiError::InvalidExtrinsic
+			})?;
+			let local_xcm = pallet_xcm::Pallet::<Runtime>::recorded_xcm();
+			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
+			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
+			Ok(ExtrinsicDryRunEffects {
+				local_xcm: local_xcm.map(VersionedXcm::<()>::V4),
+				forwarded_xcms,
+				emitted_events: events,
+				execution_result: result,
+			})
+		}
+
+		fn dry_run_xcm(origin_location: VersionedLocation, program: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			use xcm_builder::InspectMessageQueues;
+			use xcm::prelude::*;
+
+			let origin_location: Location = origin_location.try_into().map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					"Location version conversion failed with error: {:?}",
+					error,
+				);
+				XcmDryRunApiError::VersionedConversionFailed
+			})?;
+			let program: Xcm<RuntimeCall> = program.try_into().map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					"Xcm version conversion failed with error {:?}",
+					error,
+				);
+				XcmDryRunApiError::VersionedConversionFailed
+			})?;
+			let mut hash = program.using_encoded(sp_core::hashing::blake2_256);
+			let result = xcm_executor::XcmExecutor::<xcm_config::XcmConfig>::prepare_and_execute(
+				origin_location,
+				program,
+				&mut hash,
+				Weight::MAX, // Max limit available for execution.
+				Weight::zero(),
+			);
+			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
+			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
+			Ok(XcmDryRunEffects {
+				forwarded_xcms,
+				emitted_events: events,
+				execution_result: result,
+			})
+		}
+	}
+
 	impl pallet_transaction_payment_rpc_runtime_api::TransactionPaymentCallApi<Block, Balance, RuntimeCall>
 		for Runtime
 	{
@@ -1374,45 +1484,6 @@ impl_runtime_apis! {
 		}
 	}
 
-	impl xcm_fee_payment_runtime_api::XcmPaymentApi<Block> for Runtime {
-		fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
-			let acceptable = vec![
-				// native token
-				VersionedAssetId::from(AssetId(xcm_config::WestendLocation::get()))
-			];
-
-			Ok(acceptable
-				.into_iter()
-				.filter_map(|asset| asset.into_version(xcm_version).ok())
-				.collect())
-		}
-
-		fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result<u128, XcmPaymentApiError> {
-			match asset.try_as::<AssetId>() {
-				Ok(asset_id) if asset_id.0 == xcm_config::WestendLocation::get() => {
-					// for native token
-					Ok(WeightToFee::weight_to_fee(&weight))
-				},
-				Ok(asset_id) => {
-					log::trace!(target: "xcm::xcm_fee_payment_runtime_api", "query_weight_to_asset_fee - unhandled asset_id: {asset_id:?}!");
-					Err(XcmPaymentApiError::AssetNotFound)
-				},
-				Err(_) => {
-					log::trace!(target: "xcm::xcm_fee_payment_runtime_api", "query_weight_to_asset_fee - failed to convert asset: {asset:?}!");
-					Err(XcmPaymentApiError::VersionedConversionFailed)
-				}
-			}
-		}
-
-		fn query_xcm_weight(message: VersionedXcm<()>) -> Result<Weight, XcmPaymentApiError> {
-			PolkadotXcm::query_xcm_weight(message)
-		}
-
-		fn query_delivery_fees(destination: VersionedLocation, message: VersionedXcm<()>) -> Result<VersionedAssets, XcmPaymentApiError> {
-			PolkadotXcm::query_delivery_fees(destination, message)
-		}
-	}
-
 	impl cumulus_primitives_core::CollectCollationInfo<Block> for Runtime {
 		fn collect_collation_info(header: &<Block as BlockT>::Header) -> cumulus_primitives_core::CollationInfo {
 			ParachainSystem::collect_collation_info(header)
diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs
index d610bfd768cdf43c7efce3ee368b6490fcf054b1..35a42627ad71004642b278bba1ff1a564929061f 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs
@@ -440,6 +440,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 /// Local origins on this chain are allowed to dispatch XCM sends/executions.
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs
index bd1445bee22c3246bf07ba068a8ba0a0490d0770..a0d2e91dffd2e9240ad9364e18ef52a7b3cab3b1 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/xcm_config.rs
@@ -239,6 +239,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 pub type PriceForParentDelivery =
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/xcm_config.rs
index f147cd9653fe7e3eee03bf08a07cf6d3c1d94999..c2ca8e47f2a61cf6d66613daa205caddafe192b0 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/xcm_config.rs
@@ -206,6 +206,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 pub type PriceForParentDelivery =
diff --git a/cumulus/parachains/runtimes/collectives/collectives-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/collectives/collectives-westend/src/xcm_config.rs
index 84697c3e36341218ad959f575262c0903f5ae96d..c68f230a16dc3d35b861df5aa7667d61d4cf53cf 100644
--- a/cumulus/parachains/runtimes/collectives/collectives-westend/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/collectives/collectives-westend/src/xcm_config.rs
@@ -220,6 +220,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 /// Converts a local signed origin into an XCM location.
diff --git a/cumulus/parachains/runtimes/contracts/contracts-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/contracts/contracts-rococo/src/xcm_config.rs
index ac15ac5b0f0f524c560f50dcd5ea7a73e2051ae3..8c33710198605f05ec2f3f71c413e03190a3616c 100644
--- a/cumulus/parachains/runtimes/contracts/contracts-rococo/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/contracts/contracts-rococo/src/xcm_config.rs
@@ -202,6 +202,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 /// Converts a local signed origin into an XCM location.
diff --git a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/xcm_config.rs
index 9095b5b1caaa6f7655ec1f2a0ab6302b20367679..c16b40b8675fbce2878bca4ba1106b89bbd9e9b1 100644
--- a/cumulus/parachains/runtimes/coretime/coretime-rococo/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/coretime/coretime-rococo/src/xcm_config.rs
@@ -224,6 +224,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 /// Converts a local signed origin into an XCM location. Forms the basis for local origins
diff --git a/cumulus/parachains/runtimes/coretime/coretime-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/coretime/coretime-westend/src/xcm_config.rs
index defc57e2d7f552a86421359b25d64a2bacbd3572..b12765870bfdbbc7eb495a52e9ec4e931dc442da 100644
--- a/cumulus/parachains/runtimes/coretime/coretime-westend/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/coretime/coretime-westend/src/xcm_config.rs
@@ -232,6 +232,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 /// Converts a local signed origin into an XCM location. Forms the basis for local origins
diff --git a/cumulus/parachains/runtimes/glutton/glutton-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/glutton/glutton-westend/src/xcm_config.rs
index 9d438a41f8fe75037a4c9781653d5f65eee77f9c..d1fb50c1ab095cb07eb3f849c5e5d8be42fd1fc6 100644
--- a/cumulus/parachains/runtimes/glutton/glutton-westend/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/glutton/glutton-westend/src/xcm_config.rs
@@ -91,6 +91,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = ();
 }
 
 impl cumulus_pallet_xcm::Config for Runtime {
diff --git a/cumulus/parachains/runtimes/people/people-rococo/src/xcm_config.rs b/cumulus/parachains/runtimes/people/people-rococo/src/xcm_config.rs
index 101d9a180e5feaa36d65afcd01276b86768f4ab0..cca964fb2441bbe8bd40d909eb90766907918df2 100644
--- a/cumulus/parachains/runtimes/people/people-rococo/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/people/people-rococo/src/xcm_config.rs
@@ -230,6 +230,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 /// Converts a local signed origin into an XCM location. Forms the basis for local origins
diff --git a/cumulus/parachains/runtimes/people/people-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/people/people-westend/src/xcm_config.rs
index 0a903f915056313c5650f030a3c0266eff1612e1..3926ddcf21efe09933e85552b3fc31f368d69e9b 100644
--- a/cumulus/parachains/runtimes/people/people-westend/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/people/people-westend/src/xcm_config.rs
@@ -238,6 +238,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 /// Converts a local signed origin into an XCM location. Forms the basis for local origins
diff --git a/cumulus/parachains/runtimes/starters/shell/src/xcm_config.rs b/cumulus/parachains/runtimes/starters/shell/src/xcm_config.rs
index 7f9de0f64b35676f4684b15590a243e4550885d2..741b3bcd752f5511b09add16865a5f311e3626cc 100644
--- a/cumulus/parachains/runtimes/starters/shell/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/starters/shell/src/xcm_config.rs
@@ -91,6 +91,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = ();
 }
 
 impl cumulus_pallet_xcm::Config for Runtime {
diff --git a/cumulus/parachains/runtimes/testing/penpal/Cargo.toml b/cumulus/parachains/runtimes/testing/penpal/Cargo.toml
index 028aa002a91e508c858c7ba71c981d9c9cf8dc60..4ebb95f26cf6ab15b5576a62fe56e2fe626dd6d9 100644
--- a/cumulus/parachains/runtimes/testing/penpal/Cargo.toml
+++ b/cumulus/parachains/runtimes/testing/penpal/Cargo.toml
@@ -64,6 +64,7 @@ polkadot-runtime-common = { path = "../../../../../polkadot/runtime/common", def
 xcm = { package = "staging-xcm", path = "../../../../../polkadot/xcm", default-features = false }
 xcm-builder = { package = "staging-xcm-builder", path = "../../../../../polkadot/xcm/xcm-builder", default-features = false }
 xcm-executor = { package = "staging-xcm-executor", path = "../../../../../polkadot/xcm/xcm-executor", default-features = false }
+xcm-fee-payment-runtime-api = { path = "../../../../../polkadot/xcm/xcm-fee-payment-runtime-api", default-features = false }
 
 # Cumulus
 cumulus-pallet-aura-ext = { path = "../../../../pallets/aura-ext", default-features = false }
@@ -134,6 +135,7 @@ std = [
 	"substrate-wasm-builder",
 	"xcm-builder/std",
 	"xcm-executor/std",
+	"xcm-fee-payment-runtime-api/std",
 	"xcm/std",
 ]
 
@@ -164,6 +166,7 @@ runtime-benchmarks = [
 	"sp-runtime/runtime-benchmarks",
 	"xcm-builder/runtime-benchmarks",
 	"xcm-executor/runtime-benchmarks",
+	"xcm-fee-payment-runtime-api/runtime-benchmarks",
 ]
 
 try-runtime = [
diff --git a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs
index 89885d77378ba2c1df13116b5f2d41f547002b30..582154fec6d274e048558fec43893aee7d4f2817 100644
--- a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs
+++ b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs
@@ -32,6 +32,7 @@ include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
 mod weights;
 pub mod xcm_config;
 
+use codec::Encode;
 use cumulus_pallet_parachain_system::RelayNumberStrictlyIncreases;
 use cumulus_primitives_core::{AggregateMessageOrigin, ParaId};
 use frame_support::{
@@ -44,7 +45,7 @@ use frame_support::{
 		AsEnsureOriginWithArg, ConstBool, ConstU32, ConstU64, ConstU8, Everything, TransformOrigin,
 	},
 	weights::{
-		constants::WEIGHT_REF_TIME_PER_SECOND, ConstantMultiplier, FeePolynomial,
+		constants::WEIGHT_REF_TIME_PER_SECOND, ConstantMultiplier, FeePolynomial, WeightToFee as _,
 		WeightToFeeCoefficient, WeightToFeeCoefficients, WeightToFeePolynomial,
 	},
 	PalletId,
@@ -80,7 +81,14 @@ pub use sp_runtime::BuildStorage;
 use parachains_common::{AccountId, Signature};
 use polkadot_runtime_common::{BlockHashCount, SlowAdjustingFeeUpdate};
 use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight};
-use xcm::latest::prelude::{AssetId as AssetLocationId, BodyId};
+use xcm::{
+	latest::prelude::{AssetId as AssetLocationId, BodyId},
+	IntoVersion, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm,
+};
+use xcm_fee_payment_runtime_api::{
+	dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects},
+	fees::Error as XcmPaymentApiError,
+};
 
 /// Balance of an account.
 pub type Balance = u128;
@@ -835,6 +843,101 @@ impl_runtime_apis! {
 		}
 	}
 
+	impl xcm_fee_payment_runtime_api::fees::XcmPaymentApi<Block> for Runtime {
+		fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
+			if !matches!(xcm_version, 3 | 4) {
+				return Err(XcmPaymentApiError::UnhandledXcmVersion);
+			}
+			Ok([VersionedAssetId::V4(xcm_config::RelayLocation::get().into())]
+				.into_iter()
+				.filter_map(|asset| asset.into_version(xcm_version).ok())
+				.collect())
+		}
+
+		fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result<u128, XcmPaymentApiError> {
+			let local_asset = VersionedAssetId::V4(xcm_config::RelayLocation::get().into());
+			let asset = asset
+				.into_version(4)
+				.map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?;
+
+			if  asset != local_asset { return Err(XcmPaymentApiError::AssetNotFound); }
+
+			Ok(WeightToFee::weight_to_fee(&weight))
+		}
+
+		fn query_xcm_weight(message: VersionedXcm<()>) -> Result<Weight, XcmPaymentApiError> {
+			PolkadotXcm::query_xcm_weight(message)
+		}
+
+		fn query_delivery_fees(destination: VersionedLocation, message: VersionedXcm<()>) -> Result<VersionedAssets, XcmPaymentApiError> {
+			PolkadotXcm::query_delivery_fees(destination, message)
+		}
+	}
+
+	impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi<Block, RuntimeCall, RuntimeEvent> for Runtime {
+		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			use xcm_builder::InspectMessageQueues;
+			use xcm_executor::RecordXcm;
+			use xcm::prelude::*;
+
+			pallet_xcm::Pallet::<Runtime>::set_record_xcm(true);
+			let result = Executive::apply_extrinsic(extrinsic).map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_extrinsic",
+					"Applying extrinsic failed with error {:?}",
+					error,
+				);
+				XcmDryRunApiError::InvalidExtrinsic
+			})?;
+			let local_xcm = pallet_xcm::Pallet::<Runtime>::recorded_xcm();
+			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
+			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
+			Ok(ExtrinsicDryRunEffects {
+				local_xcm: local_xcm.map(VersionedXcm::<()>::V4),
+				forwarded_xcms,
+				emitted_events: events,
+				execution_result: result,
+			})
+		}
+
+		fn dry_run_xcm(origin_location: VersionedLocation, program: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			use xcm_builder::InspectMessageQueues;
+			use xcm::prelude::*;
+
+			let origin_location: Location = origin_location.try_into().map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					"Location version conversion failed with error: {:?}",
+					error,
+				);
+				XcmDryRunApiError::VersionedConversionFailed
+			})?;
+			let program: Xcm<RuntimeCall> = program.try_into().map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					"Xcm version conversion failed with error {:?}",
+					error,
+				);
+				XcmDryRunApiError::VersionedConversionFailed
+			})?;
+			let mut hash = program.using_encoded(sp_core::hashing::blake2_256);
+			let result = xcm_executor::XcmExecutor::<xcm_config::XcmConfig>::prepare_and_execute(
+				origin_location,
+				program,
+				&mut hash,
+				Weight::MAX, // Max limit.
+				Weight::zero(),
+			);
+			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
+			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
+			Ok(XcmDryRunEffects {
+				forwarded_xcms,
+				emitted_events: events,
+				execution_result: result,
+			})
+		}
+	}
+
 	#[cfg(feature = "try-runtime")]
 	impl frame_try_runtime::TryRuntime<Block> for Runtime {
 		fn on_runtime_upgrade(checks: frame_try_runtime::UpgradeCheckSelect) -> (Weight, Weight) {
diff --git a/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs b/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs
index 711041f6d6e28ccfc53d83ee2b903f06ae65de36..08a2da260c57e67b17eecf55f3845049e371b996 100644
--- a/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs
+++ b/cumulus/parachains/runtimes/testing/penpal/src/xcm_config.rs
@@ -362,6 +362,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 /// Multiplier used for dedicated `TakeFirstAssetTrader` with `ForeignAssets` instance.
diff --git a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs
index e762cec9093b3f5977d60a17fd73660e0e597dc7..f22e900ba9efd66f4abe5561442755e5ce0b5b78 100644
--- a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs
+++ b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs
@@ -490,6 +490,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 /// Local origins on this chain are allowed to dispatch XCM sends/executions.
diff --git a/cumulus/primitives/utility/src/lib.rs b/cumulus/primitives/utility/src/lib.rs
index 54f40bd01097c99ab82520c7fe83d13c96c698b2..64784eb36f846f7b325aaaa5ed72966e3d59a1b8 100644
--- a/cumulus/primitives/utility/src/lib.rs
+++ b/cumulus/primitives/utility/src/lib.rs
@@ -34,8 +34,8 @@ use sp_runtime::{
 	SaturatedConversion,
 };
 use sp_std::{marker::PhantomData, prelude::*};
-use xcm::{latest::prelude::*, WrapVersion};
-use xcm_builder::TakeRevenue;
+use xcm::{latest::prelude::*, VersionedLocation, VersionedXcm, WrapVersion};
+use xcm_builder::{InspectMessageQueues, TakeRevenue};
 use xcm_executor::{
 	traits::{MatchesFungibles, TransactAsset, WeightTrader},
 	AssetsInHolding,
@@ -93,6 +93,14 @@ where
 	}
 }
 
+impl<T: UpwardMessageSender + InspectMessageQueues, W, P> InspectMessageQueues
+	for ParentAsUmp<T, W, P>
+{
+	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
+		T::get_messages()
+	}
+}
+
 /// Contains information to handle refund/payment for xcm-execution
 #[derive(Clone, Eq, PartialEq, Debug)]
 struct AssetTraderRefunder {
diff --git a/polkadot/node/service/Cargo.toml b/polkadot/node/service/Cargo.toml
index 9688ab556473131d6e90075de9b6f69ba4f244e5..7c010778d50d3c1d10884562b6ea2c10fad8a272 100644
--- a/polkadot/node/service/Cargo.toml
+++ b/polkadot/node/service/Cargo.toml
@@ -206,6 +206,7 @@ runtime-benchmarks = [
 	"service/runtime-benchmarks",
 	"sp-runtime/runtime-benchmarks",
 	"westend-runtime?/runtime-benchmarks",
+	"xcm-fee-payment-runtime-api/runtime-benchmarks",
 ]
 try-runtime = [
 	"frame-support/try-runtime",
diff --git a/polkadot/node/service/src/fake_runtime_api.rs b/polkadot/node/service/src/fake_runtime_api.rs
index 89613040dca1fe306aa9734c149f79dad8bbff57..5c889552a6ae4b7708c0afb7511447fb518f8a9a 100644
--- a/polkadot/node/service/src/fake_runtime_api.rs
+++ b/polkadot/node/service/src/fake_runtime_api.rs
@@ -398,20 +398,30 @@ sp_api::impl_runtime_apis! {
 		}
 	}
 
-	impl xcm_fee_payment_runtime_api::XcmPaymentApi<Block> for Runtime {
-		fn query_acceptable_payment_assets(_: xcm::Version) -> Result<Vec<VersionedAssetId>, xcm_fee_payment_runtime_api::Error> {
+	impl xcm_fee_payment_runtime_api::fees::XcmPaymentApi<Block> for Runtime {
+		fn query_acceptable_payment_assets(_: xcm::Version) -> Result<Vec<VersionedAssetId>, xcm_fee_payment_runtime_api::fees::Error> {
 			unimplemented!()
 		}
 
-		fn query_weight_to_asset_fee(_: Weight, _: VersionedAssetId) -> Result<u128, xcm_fee_payment_runtime_api::Error> {
+		fn query_weight_to_asset_fee(_: Weight, _: VersionedAssetId) -> Result<u128, xcm_fee_payment_runtime_api::fees::Error> {
 			unimplemented!()
 		}
 
-		fn query_xcm_weight(_: VersionedXcm<()>) -> Result<Weight, xcm_fee_payment_runtime_api::Error> {
+		fn query_xcm_weight(_: VersionedXcm<()>) -> Result<Weight, xcm_fee_payment_runtime_api::fees::Error> {
 			unimplemented!()
 		}
 
-		fn query_delivery_fees(_: VersionedLocation, _: VersionedXcm<()>) -> Result<VersionedAssets, xcm_fee_payment_runtime_api::Error> {
+		fn query_delivery_fees(_: VersionedLocation, _: VersionedXcm<()>) -> Result<VersionedAssets, xcm_fee_payment_runtime_api::fees::Error> {
+			unimplemented!()
+		}
+	}
+
+	impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi<Block, (), ()> for Runtime {
+		fn dry_run_extrinsic(_: <Block as BlockT>::Extrinsic) -> Result<xcm_fee_payment_runtime_api::dry_run::ExtrinsicDryRunEffects<()>, xcm_fee_payment_runtime_api::dry_run::Error> {
+			unimplemented!()
+		}
+
+		fn dry_run_xcm(_: VersionedLocation, _: VersionedXcm<()>) -> Result<xcm_fee_payment_runtime_api::dry_run::XcmDryRunEffects<()>, xcm_fee_payment_runtime_api::dry_run::Error> {
 			unimplemented!()
 		}
 	}
diff --git a/polkadot/runtime/common/src/xcm_sender.rs b/polkadot/runtime/common/src/xcm_sender.rs
index a712d4381f75f586ada72b3046925d97a1fba1db..cbec1a8ca1036117275b5234229c2cfc06481056 100644
--- a/polkadot/runtime/common/src/xcm_sender.rs
+++ b/polkadot/runtime/common/src/xcm_sender.rs
@@ -18,7 +18,7 @@
 
 use frame_support::traits::Get;
 use frame_system::pallet_prelude::BlockNumberFor;
-use parity_scale_codec::Encode;
+use parity_scale_codec::{Decode, Encode};
 use primitives::Id as ParaId;
 use runtime_parachains::{
 	configuration::{self, HostConfiguration},
@@ -27,6 +27,7 @@ use runtime_parachains::{
 use sp_runtime::FixedPointNumber;
 use sp_std::{marker::PhantomData, prelude::*};
 use xcm::prelude::*;
+use xcm_builder::InspectMessageQueues;
 use SendError::*;
 
 /// Simple value-bearing trait for determining/expressing the assets required to be paid for a
@@ -138,6 +139,24 @@ where
 	}
 }
 
+impl<T: dmp::Config, W, P> InspectMessageQueues for ChildParachainRouter<T, W, P> {
+	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
+		dmp::DownwardMessageQueues::<T>::iter()
+			.map(|(para_id, messages)| {
+				let decoded_messages: Vec<VersionedXcm<()>> = messages
+					.iter()
+					.map(|downward_message| {
+						let message = VersionedXcm::<()>::decode(&mut &downward_message.msg[..]).unwrap();
+						log::trace!(target: "xcm::DownwardMessageQueues::get_messages", "Message: {:?}, sent at: {:?}", message, downward_message.sent_at);
+						message
+					})
+					.collect();
+				(VersionedLocation::V4(Parachain(para_id.into()).into()), decoded_messages)
+			})
+			.collect()
+	}
+}
+
 /// Implementation of `xcm_builder::EnsureDelivery` which helps to ensure delivery to the
 /// `ParaId` parachain (sibling or child). Deposits existential deposit for origin (if needed).
 /// Deposits estimated fee to the origin account (if needed).
diff --git a/polkadot/runtime/parachains/src/dmp.rs b/polkadot/runtime/parachains/src/dmp.rs
index 354b16cc3f082f2f74ad276e8139e21d4094c4cc..df2f93e194214c7bec474668f197ceaca53818ed 100644
--- a/polkadot/runtime/parachains/src/dmp.rs
+++ b/polkadot/runtime/parachains/src/dmp.rs
@@ -119,7 +119,7 @@ pub mod pallet {
 
 	/// The downward messages addressed for a certain para.
 	#[pallet::storage]
-	pub(crate) type DownwardMessageQueues<T: Config> = StorageMap<
+	pub type DownwardMessageQueues<T: Config> = StorageMap<
 		_,
 		Twox64Concat,
 		ParaId,
diff --git a/polkadot/runtime/rococo/Cargo.toml b/polkadot/runtime/rococo/Cargo.toml
index bbe19310f970ade37e25f5c1e2c983758c617412..f4d8fb51b3fa2bfa95bd409cd8bd7a2aec17c1f8 100644
--- a/polkadot/runtime/rococo/Cargo.toml
+++ b/polkadot/runtime/rococo/Cargo.toml
@@ -268,6 +268,7 @@ runtime-benchmarks = [
 	"sp-staking/runtime-benchmarks",
 	"xcm-builder/runtime-benchmarks",
 	"xcm-executor/runtime-benchmarks",
+	"xcm-fee-payment-runtime-api/runtime-benchmarks",
 ]
 try-runtime = [
 	"frame-executive/try-runtime",
diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs
index 3b2cbc88dc3fbcb24f8d819237ee9d51231093c0..22e6183e59460899e6eceb938d93222798ee3a34 100644
--- a/polkadot/runtime/rococo/src/lib.rs
+++ b/polkadot/runtime/rococo/src/lib.rs
@@ -134,7 +134,10 @@ use governance::{
 	pallet_custom_origins, AuctionAdmin, Fellows, GeneralAdmin, LeaseAdmin, Treasurer,
 	TreasurySpender,
 };
-use xcm_fee_payment_runtime_api::Error as XcmPaymentApiError;
+use xcm_fee_payment_runtime_api::{
+	dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects},
+	fees::Error as XcmPaymentApiError,
+};
 
 #[cfg(test)]
 mod tests;
@@ -1764,7 +1767,7 @@ sp_api::impl_runtime_apis! {
 		}
 	}
 
-	impl xcm_fee_payment_runtime_api::XcmPaymentApi<Block> for Runtime {
+	impl xcm_fee_payment_runtime_api::fees::XcmPaymentApi<Block> for Runtime {
 		fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
 			let acceptable = vec![
 				// native token
@@ -1803,6 +1806,66 @@ sp_api::impl_runtime_apis! {
 		}
 	}
 
+	impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi<Block, RuntimeCall, RuntimeEvent> for Runtime {
+		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			use xcm_builder::InspectMessageQueues;
+			use xcm_executor::RecordXcm;
+			pallet_xcm::Pallet::<Runtime>::set_record_xcm(true);
+			let result = Executive::apply_extrinsic(extrinsic).map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_extrinsic",
+					"Applying extrinsic failed with error {:?}",
+					error,
+				);
+				XcmDryRunApiError::InvalidExtrinsic
+			})?;
+			let local_xcm = pallet_xcm::Pallet::<Runtime>::recorded_xcm();
+			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
+			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
+			Ok(ExtrinsicDryRunEffects {
+				local_xcm: local_xcm.map(VersionedXcm::<()>::V4),
+				forwarded_xcms,
+				emitted_events: events,
+				execution_result: result,
+			})
+		}
+
+		fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			use xcm_builder::InspectMessageQueues;
+			let origin_location: Location = origin_location.try_into().map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					"Location version conversion failed with error: {:?}",
+					error,
+				);
+				XcmDryRunApiError::VersionedConversionFailed
+			})?;
+			let xcm: Xcm<RuntimeCall> = xcm.try_into().map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					"Xcm version conversion failed with error {:?}",
+					error,
+				);
+				XcmDryRunApiError::VersionedConversionFailed
+			})?;
+			let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256);
+			let result = xcm_executor::XcmExecutor::<xcm_config::XcmConfig>::prepare_and_execute(
+				origin_location,
+				xcm,
+				&mut hash,
+				Weight::MAX, // Max limit available for execution.
+				Weight::zero(),
+			);
+			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
+			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
+			Ok(XcmDryRunEffects {
+				forwarded_xcms,
+				emitted_events: events,
+				execution_result: result,
+			})
+		}
+	}
+
 	impl sp_api::Metadata<Block> for Runtime {
 		fn metadata() -> OpaqueMetadata {
 			OpaqueMetadata::new(Runtime::metadata().into())
diff --git a/polkadot/runtime/rococo/src/xcm_config.rs b/polkadot/runtime/rococo/src/xcm_config.rs
index c7063bd7ad616c2e5cb7b2e3f8c397087e522c33..decbc795143f006ee50cc98d860e795165269cf5 100644
--- a/polkadot/runtime/rococo/src/xcm_config.rs
+++ b/polkadot/runtime/rococo/src/xcm_config.rs
@@ -224,6 +224,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = XcmPallet;
 }
 
 parameter_types! {
diff --git a/polkadot/runtime/test-runtime/src/xcm_config.rs b/polkadot/runtime/test-runtime/src/xcm_config.rs
index 8411b79f2529d8e2d26815bd9ab806457e4ca99b..fc3d0dc42a3b93408f15c8363b631aa9761cbe36 100644
--- a/polkadot/runtime/test-runtime/src/xcm_config.rs
+++ b/polkadot/runtime/test-runtime/src/xcm_config.rs
@@ -156,6 +156,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = ();
 }
 
 impl pallet_xcm::Config for crate::Runtime {
diff --git a/polkadot/runtime/westend/Cargo.toml b/polkadot/runtime/westend/Cargo.toml
index d726adfb8e6e439edd2b5f378f5b5160ac70e6be..f02cae0e9d493834b3c0ebe5d33fb3dbc92b9d9a 100644
--- a/polkadot/runtime/westend/Cargo.toml
+++ b/polkadot/runtime/westend/Cargo.toml
@@ -283,6 +283,7 @@ runtime-benchmarks = [
 	"sp-staking/runtime-benchmarks",
 	"xcm-builder/runtime-benchmarks",
 	"xcm-executor/runtime-benchmarks",
+	"xcm-fee-payment-runtime-api/runtime-benchmarks",
 ]
 try-runtime = [
 	"frame-election-provider-support/try-runtime",
diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs
index 8ae95e6e1a83af1e5a7a586f6a206e62d413fbeb..cae12ab49c0242903a99442bea01b02bcecb532c 100644
--- a/polkadot/runtime/westend/src/lib.rs
+++ b/polkadot/runtime/westend/src/lib.rs
@@ -108,7 +108,10 @@ use xcm::{
 };
 use xcm_builder::PayOverXcm;
 
-use xcm_fee_payment_runtime_api::Error as XcmPaymentApiError;
+use xcm_fee_payment_runtime_api::{
+	dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunEffects},
+	fees::Error as XcmPaymentApiError,
+};
 
 pub use frame_system::Call as SystemCall;
 pub use pallet_balances::Call as BalancesCall;
@@ -2198,7 +2201,7 @@ sp_api::impl_runtime_apis! {
 		}
 	}
 
-	impl xcm_fee_payment_runtime_api::XcmPaymentApi<Block> for Runtime {
+	impl xcm_fee_payment_runtime_api::fees::XcmPaymentApi<Block> for Runtime {
 		fn query_acceptable_payment_assets(xcm_version: xcm::Version) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
 			let acceptable = vec![
 				// native token
@@ -2237,6 +2240,66 @@ sp_api::impl_runtime_apis! {
 		}
 	}
 
+	impl xcm_fee_payment_runtime_api::dry_run::XcmDryRunApi<Block, RuntimeCall, RuntimeEvent> for Runtime {
+		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			use xcm_builder::InspectMessageQueues;
+			use xcm_executor::RecordXcm;
+			pallet_xcm::Pallet::<Runtime>::set_record_xcm(true);
+			let result = Executive::apply_extrinsic(extrinsic).map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_extrinsic",
+					"Applying extrinsic failed with error {:?}",
+					error,
+				);
+				XcmDryRunApiError::InvalidExtrinsic
+			})?;
+			let local_xcm = pallet_xcm::Pallet::<Runtime>::recorded_xcm();
+			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
+			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
+			Ok(ExtrinsicDryRunEffects {
+				local_xcm: local_xcm.map(VersionedXcm::<()>::V4),
+				forwarded_xcms,
+				emitted_events: events,
+				execution_result: result,
+			})
+		}
+
+		fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			use xcm_builder::InspectMessageQueues;
+			let origin_location: Location = origin_location.try_into().map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					"Location version conversion failed with error: {:?}",
+					error,
+				);
+				XcmDryRunApiError::VersionedConversionFailed
+			})?;
+			let xcm: Xcm<RuntimeCall> = xcm.try_into().map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					"Xcm version conversion failed with error {:?}",
+					error,
+				);
+				XcmDryRunApiError::VersionedConversionFailed
+			})?;
+			let mut hash = xcm.using_encoded(sp_io::hashing::blake2_256);
+			let result = xcm_executor::XcmExecutor::<xcm_config::XcmConfig>::prepare_and_execute(
+				origin_location,
+				xcm,
+				&mut hash,
+				Weight::MAX, // Max limit available for execution.
+				Weight::zero(),
+			);
+			let forwarded_xcms = xcm_config::XcmRouter::get_messages();
+			let events: Vec<RuntimeEvent> = System::read_events_no_consensus().map(|record| record.event.clone()).collect();
+			Ok(XcmDryRunEffects {
+				forwarded_xcms,
+				emitted_events: events,
+				execution_result: result,
+			})
+		}
+	}
+
 	impl pallet_nomination_pools_runtime_api::NominationPoolsApi<
 		Block,
 		AccountId,
diff --git a/polkadot/runtime/westend/src/xcm_config.rs b/polkadot/runtime/westend/src/xcm_config.rs
index f661c4b0e4f437a2f2d597d8bdef26e0b76930c3..c6c5fb9e72a46afa70c6db09fd3a2b0f38d7e3a5 100644
--- a/polkadot/runtime/westend/src/xcm_config.rs
+++ b/polkadot/runtime/westend/src/xcm_config.rs
@@ -222,6 +222,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = XcmPallet;
 }
 
 parameter_types! {
diff --git a/polkadot/xcm/pallet-xcm-benchmarks/src/fungible/mock.rs b/polkadot/xcm/pallet-xcm-benchmarks/src/fungible/mock.rs
index 7233b46d0cd64d2f649fce9c007da1c1753bcff9..c0dfa91afc7866500b8761753fb83c76b5b23069 100644
--- a/polkadot/xcm/pallet-xcm-benchmarks/src/fungible/mock.rs
+++ b/polkadot/xcm/pallet-xcm-benchmarks/src/fungible/mock.rs
@@ -120,6 +120,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = ();
 }
 
 impl crate::Config for Test {
diff --git a/polkadot/xcm/pallet-xcm-benchmarks/src/generic/mock.rs b/polkadot/xcm/pallet-xcm-benchmarks/src/generic/mock.rs
index a9f4d37d7a55f5fa50cd6767447c2578774a7017..f51d34092616b6191cefcb82803673f43f42e4c3 100644
--- a/polkadot/xcm/pallet-xcm-benchmarks/src/generic/mock.rs
+++ b/polkadot/xcm/pallet-xcm-benchmarks/src/generic/mock.rs
@@ -110,6 +110,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = ();
 }
 
 parameter_types! {
diff --git a/polkadot/xcm/pallet-xcm/Cargo.toml b/polkadot/xcm/pallet-xcm/Cargo.toml
index 460597e6649ab27acec712e1b055fbf5968005f2..fc4d23426fbcce3f5da855c4bc157b6dd986376d 100644
--- a/polkadot/xcm/pallet-xcm/Cargo.toml
+++ b/polkadot/xcm/pallet-xcm/Cargo.toml
@@ -69,6 +69,7 @@ runtime-benchmarks = [
 	"sp-runtime/runtime-benchmarks",
 	"xcm-builder/runtime-benchmarks",
 	"xcm-executor/runtime-benchmarks",
+	"xcm-fee-payment-runtime-api/runtime-benchmarks",
 ]
 try-runtime = [
 	"frame-support/try-runtime",
diff --git a/polkadot/xcm/pallet-xcm/src/lib.rs b/polkadot/xcm/pallet-xcm/src/lib.rs
index af3b66121ea131bba2b8a0e694e59763cd7eb4cc..37fc121ba2174c95f3a8ac82131d525ff8279f48 100644
--- a/polkadot/xcm/pallet-xcm/src/lib.rs
+++ b/polkadot/xcm/pallet-xcm/src/lib.rs
@@ -61,7 +61,7 @@ use xcm_executor::{
 	},
 	AssetsInHolding,
 };
-use xcm_fee_payment_runtime_api::Error as FeePaymentError;
+use xcm_fee_payment_runtime_api::fees::Error as XcmPaymentApiError;
 
 #[cfg(any(feature = "try-runtime", test))]
 use sp_runtime::TryRuntimeError;
@@ -764,6 +764,25 @@ pub mod pallet {
 	#[pallet::storage]
 	pub(super) type XcmExecutionSuspended<T: Config> = StorageValue<_, bool, ValueQuery>;
 
+	/// Whether or not incoming XCMs (both executed locally and received) should be recorded.
+	/// Only one XCM program will be recorded at a time.
+	/// This is meant to be used in runtime APIs, and it's advised it stays false
+	/// for all other use cases, so as to not degrade regular performance.
+	///
+	/// Only relevant if this pallet is being used as the [`xcm_executor::traits::RecordXcm`]
+	/// implementation in the XCM executor configuration.
+	#[pallet::storage]
+	pub(crate) type ShouldRecordXcm<T: Config> = StorageValue<_, bool, ValueQuery>;
+
+	/// If [`ShouldRecordXcm`] is set to true, then the last XCM program executed locally
+	/// will be stored here.
+	/// Runtime APIs can fetch the XCM that was executed by accessing this value.
+	///
+	/// Only relevant if this pallet is being used as the [`xcm_executor::traits::RecordXcm`]
+	/// implementation in the XCM executor configuration.
+	#[pallet::storage]
+	pub(crate) type RecordedXcm<T: Config> = StorageValue<_, Xcm<()>>;
+
 	#[pallet::genesis_config]
 	pub struct GenesisConfig<T: Config> {
 		#[serde(skip)]
@@ -2413,35 +2432,37 @@ impl<T: Config> Pallet<T> {
 		AccountIdConversion::<T::AccountId>::into_account_truncating(&ID)
 	}
 
-	pub fn query_xcm_weight(message: VersionedXcm<()>) -> Result<Weight, FeePaymentError> {
-		let message =
-			Xcm::<()>::try_from(message).map_err(|_| FeePaymentError::VersionedConversionFailed)?;
+	pub fn query_xcm_weight(message: VersionedXcm<()>) -> Result<Weight, XcmPaymentApiError> {
+		let message = Xcm::<()>::try_from(message)
+			.map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?;
 
 		T::Weigher::weight(&mut message.into()).map_err(|()| {
 			log::error!(target: "xcm::pallet_xcm::query_xcm_weight", "Error when querying XCM weight");
-			FeePaymentError::WeightNotComputable
+			XcmPaymentApiError::WeightNotComputable
 		})
 	}
 
 	pub fn query_delivery_fees(
 		destination: VersionedLocation,
 		message: VersionedXcm<()>,
-	) -> Result<VersionedAssets, FeePaymentError> {
+	) -> Result<VersionedAssets, XcmPaymentApiError> {
 		let result_version = destination.identify_version().max(message.identify_version());
 
-		let destination =
-			destination.try_into().map_err(|_| FeePaymentError::VersionedConversionFailed)?;
+		let destination = destination
+			.try_into()
+			.map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?;
 
-		let message = message.try_into().map_err(|_| FeePaymentError::VersionedConversionFailed)?;
+		let message =
+			message.try_into().map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?;
 
 		let (_, fees) = validate_send::<T::XcmRouter>(destination, message).map_err(|error| {
 			log::error!(target: "xcm::pallet_xcm::query_delivery_fees", "Error when querying delivery fees: {:?}", error);
-			FeePaymentError::Unroutable
+			XcmPaymentApiError::Unroutable
 		})?;
 
 		VersionedAssets::from(fees)
 			.into_version(result_version)
-			.map_err(|_| FeePaymentError::VersionedConversionFailed)
+			.map_err(|_| XcmPaymentApiError::VersionedConversionFailed)
 	}
 
 	/// Create a new expectation of a query response with the querier being here.
@@ -3105,6 +3126,24 @@ impl<T: Config> CheckSuspension for Pallet<T> {
 	}
 }
 
+impl<T: Config> xcm_executor::traits::RecordXcm for Pallet<T> {
+	fn should_record() -> bool {
+		ShouldRecordXcm::<T>::get()
+	}
+
+	fn set_record_xcm(enabled: bool) {
+		ShouldRecordXcm::<T>::put(enabled);
+	}
+
+	fn recorded_xcm() -> Option<Xcm<()>> {
+		RecordedXcm::<T>::get()
+	}
+
+	fn record(xcm: Xcm<()>) {
+		RecordedXcm::<T>::put(xcm);
+	}
+}
+
 /// Ensure that the origin `o` represents an XCM (`Transact`) origin.
 ///
 /// Returns `Ok` with the location of the XCM sender or an `Err` otherwise.
diff --git a/polkadot/xcm/pallet-xcm/src/mock.rs b/polkadot/xcm/pallet-xcm/src/mock.rs
index 8e94803e84315285391c0f85e5d249330a3db31b..b3b7529217f5a4929ce7beedcc1ffa423227112a 100644
--- a/polkadot/xcm/pallet-xcm/src/mock.rs
+++ b/polkadot/xcm/pallet-xcm/src/mock.rs
@@ -531,6 +531,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = XcmPallet;
 }
 
 pub type LocalOriginToLocation = SignedToAccountId32<RuntimeOrigin, AccountId, AnyNetwork>;
diff --git a/polkadot/xcm/pallet-xcm/src/tests/mod.rs b/polkadot/xcm/pallet-xcm/src/tests/mod.rs
index 782c8bed478e18181db64256471fed0dca56e6fd..02aeafd68e83dffd01a7630ce357a4e9cd78cdf1 100644
--- a/polkadot/xcm/pallet-xcm/src/tests/mod.rs
+++ b/polkadot/xcm/pallet-xcm/src/tests/mod.rs
@@ -21,8 +21,8 @@ pub(crate) mod assets_transfer;
 use crate::{
 	mock::*, pallet::SupportedVersion, AssetTraps, Config, CurrentMigration, Error,
 	ExecuteControllerWeightInfo, LatestVersionedLocation, Pallet, Queries, QueryStatus,
-	VersionDiscoveryQueue, VersionMigrationStage, VersionNotifiers, VersionNotifyTargets,
-	WeightInfo,
+	RecordedXcm, ShouldRecordXcm, VersionDiscoveryQueue, VersionMigrationStage, VersionNotifiers,
+	VersionNotifyTargets, WeightInfo,
 };
 use frame_support::{
 	assert_err_ignore_postinfo, assert_noop, assert_ok,
@@ -1245,3 +1245,35 @@ fn multistage_migration_works() {
 		assert!(Pallet::<Test>::do_try_state().is_ok());
 	})
 }
+
+#[test]
+fn record_xcm_works() {
+	let balances = vec![(ALICE, INITIAL_BALANCE)];
+	new_test_ext_with_balances(balances).execute_with(|| {
+		let message = Xcm::<RuntimeCall>::builder()
+			.withdraw_asset((Here, SEND_AMOUNT))
+			.buy_execution((Here, SEND_AMOUNT), Unlimited)
+			.deposit_asset(AllCounted(1), Junction::AccountId32 { network: None, id: BOB.into() })
+			.build();
+		// Test default values.
+		assert_eq!(ShouldRecordXcm::<Test>::get(), false);
+		assert_eq!(RecordedXcm::<Test>::get(), None);
+
+		// By default the message won't be recorded.
+		assert_ok!(XcmPallet::execute(
+			RuntimeOrigin::signed(ALICE),
+			Box::new(VersionedXcm::from(message.clone())),
+			BaseXcmWeight::get() * 3,
+		));
+		assert_eq!(RecordedXcm::<Test>::get(), None);
+
+		// We explicitly set the record flag to true so we record the XCM.
+		ShouldRecordXcm::<Test>::put(true);
+		assert_ok!(XcmPallet::execute(
+			RuntimeOrigin::signed(ALICE),
+			Box::new(VersionedXcm::from(message.clone())),
+			BaseXcmWeight::get() * 3,
+		));
+		assert_eq!(RecordedXcm::<Test>::get(), Some(message.into()));
+	});
+}
diff --git a/polkadot/xcm/xcm-builder/src/lib.rs b/polkadot/xcm/xcm-builder/src/lib.rs
index 1ba38d0db836b4e73e3f7785cf0e96f079666441..cc06c298a418d56f6a70d58b5f24bf8ccb14db5d 100644
--- a/polkadot/xcm/xcm-builder/src/lib.rs
+++ b/polkadot/xcm/xcm-builder/src/lib.rs
@@ -120,7 +120,9 @@ mod process_xcm_message;
 pub use process_xcm_message::ProcessXcmMessage;
 
 mod routing;
-pub use routing::{EnsureDecodableXcm, EnsureDelivery, WithTopicSource, WithUniqueTopic};
+pub use routing::{
+	EnsureDecodableXcm, EnsureDelivery, InspectMessageQueues, WithTopicSource, WithUniqueTopic,
+};
 
 mod transactional;
 pub use transactional::FrameTransactionalProcessor;
diff --git a/polkadot/xcm/xcm-builder/src/routing.rs b/polkadot/xcm/xcm-builder/src/routing.rs
index 921b9ac5922eaf37e65151b8e3a115e010b0abcb..5c284aaf1475fce0fba7e5cfa042428ceff8268d 100644
--- a/polkadot/xcm/xcm-builder/src/routing.rs
+++ b/polkadot/xcm/xcm-builder/src/routing.rs
@@ -18,7 +18,7 @@
 
 use frame_system::unique;
 use parity_scale_codec::Encode;
-use sp_std::{marker::PhantomData, result::Result};
+use sp_std::{marker::PhantomData, result::Result, vec::Vec};
 use xcm::prelude::*;
 use xcm_executor::{traits::FeeReason, FeesMode};
 
@@ -60,6 +60,11 @@ impl<Inner: SendXcm> SendXcm for WithUniqueTopic<Inner> {
 		Ok(unique_id)
 	}
 }
+impl<Inner: InspectMessageQueues> InspectMessageQueues for WithUniqueTopic<Inner> {
+	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
+		Inner::get_messages()
+	}
+}
 
 pub trait SourceTopic {
 	fn source_topic(entropy: impl Encode) -> XcmHash;
@@ -140,6 +145,26 @@ impl EnsureDelivery for Tuple {
 	}
 }
 
+/// Inspects messages in queues.
+/// Meant to be used in runtime APIs, not in runtimes.
+pub trait InspectMessageQueues {
+	/// Get queued messages and their destinations.
+	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)>;
+}
+
+#[impl_trait_for_tuples::impl_for_tuples(30)]
+impl InspectMessageQueues for Tuple {
+	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
+		let mut messages = Vec::new();
+
+		for_tuples!( #(
+			messages.append(&mut Tuple::get_messages());
+		)* );
+
+		messages
+	}
+}
+
 /// A wrapper router that attempts to *encode* and *decode* passed XCM `message` to ensure that the
 /// receiving side will be able to decode, at least with the same XCM version.
 ///
diff --git a/polkadot/xcm/xcm-builder/src/tests/mock.rs b/polkadot/xcm/xcm-builder/src/tests/mock.rs
index 7532b97d97b3ba0a8c4ac648d68accc3cadfd6cd..f45650ec5404d14780a3bff3d9bbef0d72a57a25 100644
--- a/polkadot/xcm/xcm-builder/src/tests/mock.rs
+++ b/polkadot/xcm/xcm-builder/src/tests/mock.rs
@@ -748,6 +748,7 @@ impl Config for TestConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = ();
 }
 
 pub fn fungible_multi_asset(location: Location, amount: u128) -> Asset {
diff --git a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs
index 019113a12b2fb127142c9e59d00ef0faca84b688..34b204b434d6e3e547a8b1f5ec48d787bb1fded5 100644
--- a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs
+++ b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs
@@ -221,6 +221,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = XcmPallet;
 }
 
 parameter_types! {
diff --git a/polkadot/xcm/xcm-builder/src/universal_exports.rs b/polkadot/xcm/xcm-builder/src/universal_exports.rs
index d0e3ef3032ea1c0aeae698d1e275d4a401a7bb0d..04ceb7e51688994b65701419020ffd5fd5bfdcd7 100644
--- a/polkadot/xcm/xcm-builder/src/universal_exports.rs
+++ b/polkadot/xcm/xcm-builder/src/universal_exports.rs
@@ -16,6 +16,7 @@
 
 //! Traits and utilities to help with origin mutation and bridging.
 
+use crate::InspectMessageQueues;
 use frame_support::{ensure, traits::Get};
 use parity_scale_codec::{Decode, Encode};
 use sp_std::{convert::TryInto, marker::PhantomData, prelude::*};
@@ -335,6 +336,14 @@ impl<Bridges: ExporterFor, Router: SendXcm, UniversalLocation: Get<InteriorLocat
 	}
 }
 
+impl<Bridges, Router: InspectMessageQueues, UniversalLocation> InspectMessageQueues
+	for SovereignPaidRemoteExporter<Bridges, Router, UniversalLocation>
+{
+	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
+		Router::get_messages()
+	}
+}
+
 pub trait DispatchBlob {
 	/// Takes an incoming blob from over some point-to-point link (usually from some sort of
 	/// inter-consensus bridge) and then does what needs to be done with it. Usually this means
diff --git a/polkadot/xcm/xcm-builder/tests/mock/mod.rs b/polkadot/xcm/xcm-builder/tests/mock/mod.rs
index 46ec23beebc1e33c1f3e0d3e25363944f4836cdc..45bfba2355630e4e5b40d1bb9e00114c889811a1 100644
--- a/polkadot/xcm/xcm-builder/tests/mock/mod.rs
+++ b/polkadot/xcm/xcm-builder/tests/mock/mod.rs
@@ -209,6 +209,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = XcmPallet;
 }
 
 pub type LocalOriginToLocation = SignedToAccountId32<RuntimeOrigin, AccountId, KusamaNetwork>;
diff --git a/polkadot/xcm/xcm-executor/src/config.rs b/polkadot/xcm/xcm-executor/src/config.rs
index b296d32ca2adb6761f815aa74397211e3bc17d5f..63b113bc250fa34db10336f7575771bbdbf76b01 100644
--- a/polkadot/xcm/xcm-executor/src/config.rs
+++ b/polkadot/xcm/xcm-executor/src/config.rs
@@ -17,8 +17,8 @@
 use crate::traits::{
 	AssetExchange, AssetLock, CallDispatcher, ClaimAssets, ConvertOrigin, DropAssets, ExportXcm,
 	FeeManager, HandleHrmpChannelAccepted, HandleHrmpChannelClosing,
-	HandleHrmpNewChannelOpenRequest, OnResponse, ProcessTransaction, ShouldExecute, TransactAsset,
-	VersionChangeNotifier, WeightBounds, WeightTrader,
+	HandleHrmpNewChannelOpenRequest, OnResponse, ProcessTransaction, RecordXcm, ShouldExecute,
+	TransactAsset, VersionChangeNotifier, WeightBounds, WeightTrader,
 };
 use frame_support::{
 	dispatch::{GetDispatchInfo, Parameter, PostDispatchInfo},
@@ -122,4 +122,6 @@ pub trait Config {
 	type HrmpChannelAcceptedHandler: HandleHrmpChannelAccepted;
 	/// Allows optional logic execution for the `HrmpChannelClosing` XCM notification.
 	type HrmpChannelClosingHandler: HandleHrmpChannelClosing;
+	/// Allows recording the last executed XCM (used by dry-run runtime APIs).
+	type XcmRecorder: RecordXcm;
 }
diff --git a/polkadot/xcm/xcm-executor/src/lib.rs b/polkadot/xcm/xcm-executor/src/lib.rs
index a7052328da00140faf90d5391f27d126dc953a27..e0b8a8a9c73e4aba1274993d23ac27a77c2a6c78 100644
--- a/polkadot/xcm/xcm-executor/src/lib.rs
+++ b/polkadot/xcm/xcm-executor/src/lib.rs
@@ -37,6 +37,8 @@ use traits::{
 	XcmAssetTransfers,
 };
 
+pub use traits::RecordXcm;
+
 mod assets;
 pub use assets::AssetsInHolding;
 mod config;
@@ -211,6 +213,13 @@ impl<Config: config::Config> ExecuteXcm<Config::RuntimeCall> for XcmExecutor<Con
 			"origin: {origin:?}, message: {message:?}, weight_credit: {weight_credit:?}",
 		);
 		let mut properties = Properties { weight_credit, message_id: None };
+
+		// We only want to record under certain conditions (mainly only during dry-running),
+		// so as to not degrade regular performance.
+		if Config::XcmRecorder::should_record() {
+			Config::XcmRecorder::record(message.clone().into());
+		}
+
 		if let Err(e) = Config::Barrier::should_execute(
 			&origin,
 			message.inner_mut(),
diff --git a/polkadot/xcm/xcm-executor/src/traits/mod.rs b/polkadot/xcm/xcm-executor/src/traits/mod.rs
index aa3f0d26e3025781eacefcf4b6cfdfe140cb39a5..feb2922bcdffce1ea9e3a40e22875c085c82870f 100644
--- a/polkadot/xcm/xcm-executor/src/traits/mod.rs
+++ b/polkadot/xcm/xcm-executor/src/traits/mod.rs
@@ -49,7 +49,9 @@ mod hrmp;
 pub use hrmp::{
 	HandleHrmpChannelAccepted, HandleHrmpChannelClosing, HandleHrmpNewChannelOpenRequest,
 };
+mod record_xcm;
 mod weight;
+pub use record_xcm::RecordXcm;
 #[deprecated = "Use `sp_runtime::traits::` instead"]
 pub use sp_runtime::traits::{Identity, TryConvertInto as JustTry};
 pub use weight::{WeightBounds, WeightTrader};
diff --git a/polkadot/xcm/xcm-executor/src/traits/record_xcm.rs b/polkadot/xcm/xcm-executor/src/traits/record_xcm.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d0b5bf92d795622038e0272936a00f6eaecef8b2
--- /dev/null
+++ b/polkadot/xcm/xcm-executor/src/traits/record_xcm.rs
@@ -0,0 +1,46 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Polkadot is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Polkadot is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Trait for recording XCMs and a dummy implementation.
+
+use xcm::latest::Xcm;
+
+/// Trait for recording XCMs.
+pub trait RecordXcm {
+	/// Whether or not we should record incoming XCMs.
+	fn should_record() -> bool;
+	/// Enable or disable recording.
+	fn set_record_xcm(enabled: bool);
+	/// Get recorded XCM.
+	/// Returns `None` if no message was sent, or if recording was off.
+	fn recorded_xcm() -> Option<Xcm<()>>;
+	/// Record `xcm`.
+	fn record(xcm: Xcm<()>);
+}
+
+impl RecordXcm for () {
+	fn should_record() -> bool {
+		false
+	}
+
+	fn set_record_xcm(_: bool) {}
+
+	fn recorded_xcm() -> Option<Xcm<()>> {
+		None
+	}
+
+	fn record(_: Xcm<()>) {}
+}
diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/Cargo.toml b/polkadot/xcm/xcm-fee-payment-runtime-api/Cargo.toml
index 30c7c0bac14f7936d57ec5c86f94bc24ba17013f..cec76e7327ec6145e8513a33264c9419414b0991 100644
--- a/polkadot/xcm/xcm-fee-payment-runtime-api/Cargo.toml
+++ b/polkadot/xcm/xcm-fee-payment-runtime-api/Cargo.toml
@@ -26,15 +26,46 @@ sp-weights = { path = "../../../substrate/primitives/weights", default-features
 xcm = { package = "staging-xcm", path = "../", default-features = false }
 frame-support = { path = "../../../substrate/frame/support", default-features = false }
 
+[dev-dependencies]
+frame-system = { path = "../../../substrate/frame/system", default-features = false }
+pallet-xcm = { path = "../pallet-xcm", default-features = false }
+xcm-builder = { package = "staging-xcm-builder", path = "../xcm-builder", default-features = false }
+sp-io = { path = "../../../substrate/primitives/io", default-features = false }
+pallet-balances = { path = "../../../substrate/frame/balances", default-features = false }
+pallet-assets = { path = "../../../substrate/frame/assets", default-features = false }
+xcm-executor = { package = "staging-xcm-executor", path = "../xcm-executor", default-features = false }
+frame-executive = { path = "../../../substrate/frame/executive", default-features = false }
+log = { workspace = true }
+env_logger = "0.9.0"
+
 [features]
 default = ["std"]
 std = [
 	"codec/std",
+	"frame-executive/std",
 	"frame-support/std",
+	"frame-system/std",
+	"log/std",
+	"pallet-assets/std",
+	"pallet-balances/std",
+	"pallet-xcm/std",
 	"scale-info/std",
 	"sp-api/std",
+	"sp-io/std",
 	"sp-runtime/std",
 	"sp-std/std",
 	"sp-weights/std",
+	"xcm-builder/std",
+	"xcm-executor/std",
 	"xcm/std",
 ]
+runtime-benchmarks = [
+	"frame-support/runtime-benchmarks",
+	"frame-system/runtime-benchmarks",
+	"pallet-assets/runtime-benchmarks",
+	"pallet-balances/runtime-benchmarks",
+	"pallet-xcm/runtime-benchmarks",
+	"sp-runtime/runtime-benchmarks",
+	"xcm-builder/runtime-benchmarks",
+	"xcm-executor/runtime-benchmarks",
+]
diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/src/dry_run.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/src/dry_run.rs
new file mode 100644
index 0000000000000000000000000000000000000000..62a422d6efeb0928e4f279798382dd139d95ee0a
--- /dev/null
+++ b/polkadot/xcm/xcm-fee-payment-runtime-api/src/dry_run.rs
@@ -0,0 +1,83 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Substrate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Substrate is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Runtime API definition for dry-running XCM-related extrinsics.
+//! This API can be used to simulate XCMs and, for example, find the fees
+//! that need to be paid.
+
+use codec::{Decode, Encode};
+use frame_support::pallet_prelude::{DispatchResult, TypeInfo};
+use sp_runtime::traits::Block as BlockT;
+use sp_std::vec::Vec;
+use xcm::prelude::*;
+
+/// Effects of dry-running an extrinsic.
+#[derive(Encode, Decode, Debug, TypeInfo)]
+pub struct ExtrinsicDryRunEffects<Event> {
+	/// The result of executing the extrinsic.
+	pub execution_result: DispatchResult,
+	/// The list of events fired by the extrinsic.
+	pub emitted_events: Vec<Event>,
+	/// The local XCM that was attempted to be executed, if any.
+	pub local_xcm: Option<VersionedXcm<()>>,
+	/// The list of XCMs that were queued for sending.
+	pub forwarded_xcms: Vec<(VersionedLocation, Vec<VersionedXcm<()>>)>,
+}
+
+/// Effects of dry-running an XCM program.
+#[derive(Encode, Decode, Debug, TypeInfo)]
+pub struct XcmDryRunEffects<Event> {
+	/// The outcome of the XCM program execution.
+	pub execution_result: Outcome,
+	/// List of events fired by the XCM program execution.
+	pub emitted_events: Vec<Event>,
+	/// List of queued messages for sending.
+	pub forwarded_xcms: Vec<(VersionedLocation, Vec<VersionedXcm<()>>)>,
+}
+
+sp_api::decl_runtime_apis! {
+	/// API for dry-running extrinsics and XCM programs to get the programs that need to be passed to the fees API.
+	///
+	/// All calls return a vector of tuples (location, xcm) where each "xcm" is executed in "location".
+	/// If there's local execution, the location will be "Here".
+	/// This vector can be used to calculate both execution and delivery fees.
+	///
+	/// Extrinsics or XCMs might fail when executed, this doesn't mean the result of these calls will be an `Err`.
+	/// In those cases, there might still be a valid result, with the execution error inside it.
+	/// The only reasons why these calls might return an error are listed in the [`Error`] enum.
+	pub trait XcmDryRunApi<Call, Event: Decode> {
+		/// Dry run extrinsic.
+		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<Event>, Error>;
+
+		/// Dry run XCM program
+		fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm<Call>) -> Result<XcmDryRunEffects<Event>, Error>;
+	}
+}
+
+#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo)]
+pub enum Error {
+	/// An API call is unsupported.
+	#[codec(index = 0)]
+	Unimplemented,
+
+	/// Converting a versioned data structure from one version to another failed.
+	#[codec(index = 1)]
+	VersionedConversionFailed,
+
+	/// Extrinsic was invalid.
+	#[codec(index = 2)]
+	InvalidExtrinsic,
+}
diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/src/fees.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/src/fees.rs
new file mode 100644
index 0000000000000000000000000000000000000000..572d4edf533865e66299eba7bd0d22a890547603
--- /dev/null
+++ b/polkadot/xcm/xcm-fee-payment-runtime-api/src/fees.rs
@@ -0,0 +1,97 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Substrate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Substrate is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Runtime API definition for getting XCM fees.
+
+use codec::{Decode, Encode};
+use frame_support::pallet_prelude::TypeInfo;
+use sp_std::vec::Vec;
+use sp_weights::Weight;
+use xcm::{Version, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm};
+
+sp_api::decl_runtime_apis! {
+	/// A trait of XCM payment API.
+	///
+	/// API provides functionality for obtaining:
+	///
+	/// * the weight required to execute an XCM message,
+	/// * a list of acceptable `AssetId`s for message execution payment,
+	/// * the cost of the weight in the specified acceptable `AssetId`.
+	/// * the fees for an XCM message delivery.
+	///
+	/// To determine the execution weight of the calls required for
+	/// [`xcm::latest::Instruction::Transact`] instruction, `TransactionPaymentCallApi` can be used.
+	pub trait XcmPaymentApi {
+		/// Returns a list of acceptable payment assets.
+		///
+		/// # Arguments
+		///
+		/// * `xcm_version`: Version.
+		fn query_acceptable_payment_assets(xcm_version: Version) -> Result<Vec<VersionedAssetId>, Error>;
+
+		/// Returns a weight needed to execute a XCM.
+		///
+		/// # Arguments
+		///
+		/// * `message`: `VersionedXcm`.
+		fn query_xcm_weight(message: VersionedXcm<()>) -> Result<Weight, Error>;
+
+		/// Converts a weight into a fee for the specified `AssetId`.
+		///
+		/// # Arguments
+		///
+		/// * `weight`: convertible `Weight`.
+		/// * `asset`: `VersionedAssetId`.
+		fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result<u128, Error>;
+
+		/// Get delivery fees for sending a specific `message` to a `destination`.
+		/// These always come in a specific asset, defined by the chain.
+		///
+		/// # Arguments
+		/// * `message`: The message that'll be sent, necessary because most delivery fees are based on the
+		///   size of the message.
+		/// * `destination`: The destination to send the message to. Different destinations may use
+		///   different senders that charge different fees.
+		fn query_delivery_fees(destination: VersionedLocation, message: VersionedXcm<()>) -> Result<VersionedAssets, Error>;
+	}
+}
+
+#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo)]
+pub enum Error {
+	/// An API part is unsupported.
+	#[codec(index = 0)]
+	Unimplemented,
+
+	/// Converting a versioned data structure from one version to another failed.
+	#[codec(index = 1)]
+	VersionedConversionFailed,
+
+	/// XCM message weight calculation failed.
+	#[codec(index = 2)]
+	WeightNotComputable,
+
+	/// XCM version not able to be handled.
+	#[codec(index = 3)]
+	UnhandledXcmVersion,
+
+	/// The given asset is not handled as a fee asset.
+	#[codec(index = 4)]
+	AssetNotFound,
+
+	/// Destination is known to be unroutable.
+	#[codec(index = 5)]
+	Unroutable,
+}
diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/src/lib.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/src/lib.rs
index 50fd4692cb0d3ae891395ef445609d0e3a7f0297..616ee4c2eccb0aeed1cdb5e2aa524855d8b2df82 100644
--- a/polkadot/xcm/xcm-fee-payment-runtime-api/src/lib.rs
+++ b/polkadot/xcm/xcm-fee-payment-runtime-api/src/lib.rs
@@ -14,86 +14,19 @@
 // You should have received a copy of the GNU General Public License
 // along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
 
-//! Runtime API definition for xcm transaction payment.
+//! Runtime APIs for estimating xcm fee payment.
+//! This crate offers two APIs, one for estimating fees,
+//! which can be used for any type of message, and another one
+//! for returning the specific messages used for transfers, a common
+//! feature.
+//! Users of these APIs should call the transfers API and pass the result to the
+//! fees API.
 
 #![cfg_attr(not(feature = "std"), no_std)]
 
-use codec::{Decode, Encode};
-use frame_support::pallet_prelude::TypeInfo;
-use sp_std::vec::Vec;
-use sp_weights::Weight;
-use xcm::{Version, VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm};
-
-sp_api::decl_runtime_apis! {
-	/// A trait of XCM payment API.
-	///
-	/// API provides functionality for obtaining:
-	///
-	/// * the weight required to execute an XCM message,
-	/// * a list of acceptable `AssetId`s for message execution payment,
-	/// * the cost of the weight in the specified acceptable `AssetId`.
-	/// * the fees for an XCM message delivery.
-	///
-	/// To determine the execution weight of the calls required for
-	/// [`xcm::latest::Instruction::Transact`] instruction, `TransactionPaymentCallApi` can be used.
-	pub trait XcmPaymentApi {
-		/// Returns a list of acceptable payment assets.
-		///
-		/// # Arguments
-		///
-		/// * `xcm_version`: desired XCM `Version` of `VersionedAssetId`.
-		fn query_acceptable_payment_assets(xcm_version: Version) -> Result<Vec<VersionedAssetId>, Error>;
-
-		/// Returns a weight needed to execute a XCM.
-		///
-		/// # Arguments
-		///
-		/// * `message`: `VersionedXcm`.
-		fn query_xcm_weight(message: VersionedXcm<()>) -> Result<Weight, Error>;
-
-		/// Converts a weight into a fee for the specified `AssetId`.
-		///
-		/// # Arguments
-		///
-		/// * `weight`: convertible `Weight`.
-		/// * `asset`: `VersionedAssetId`.
-		fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result<u128, Error>;
-
-		/// Get delivery fees for sending a specific `message` to a `destination`.
-		/// These always come in a specific asset, defined by the chain.
-		///
-		/// # Arguments
-		/// * `message`: The message that'll be sent, necessary because most delivery fees are based on the
-		///   size of the message.
-		/// * `destination`: The destination to send the message to. Different destinations may use
-		///   different senders that charge different fees.
-		fn query_delivery_fees(destination: VersionedLocation, message: VersionedXcm<()>) -> Result<VersionedAssets, Error>;
-	}
-}
-
-#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo)]
-pub enum Error {
-	/// An API part is unsupported.
-	#[codec(index = 0)]
-	Unimplemented,
-
-	/// Converting a versioned data structure from one version to another failed.
-	#[codec(index = 1)]
-	VersionedConversionFailed,
-
-	/// XCM message weight calculation failed.
-	#[codec(index = 2)]
-	WeightNotComputable,
-
-	/// XCM version not able to be handled.
-	#[codec(index = 3)]
-	UnhandledXcmVersion,
-
-	/// The given asset is not handled as a fee asset.
-	#[codec(index = 4)]
-	AssetNotFound,
-
-	/// Destination is known to be unroutable.
-	#[codec(index = 5)]
-	Unroutable,
-}
+/// Dry-run API.
+/// Given an extrinsic or an XCM program, it returns the outcome of its execution.
+pub mod dry_run;
+/// Fee estimation API.
+/// Given an XCM program, it will return the fees needed to execute it properly or send it.
+pub mod fees;
diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/tests/fee_estimation.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/tests/fee_estimation.rs
new file mode 100644
index 0000000000000000000000000000000000000000..7a9bfa4a7968129a13395f2520b6ca1a7b14ff69
--- /dev/null
+++ b/polkadot/xcm/xcm-fee-payment-runtime-api/tests/fee_estimation.rs
@@ -0,0 +1,370 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Substrate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Substrate is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Tests for using both the XCM fee payment API and the dry-run API.
+
+use frame_support::{
+	dispatch::DispatchInfo,
+	pallet_prelude::{DispatchClass, Pays},
+};
+use sp_api::ProvideRuntimeApi;
+use sp_runtime::testing::H256;
+use xcm::prelude::*;
+use xcm_fee_payment_runtime_api::{dry_run::XcmDryRunApi, fees::XcmPaymentApi};
+
+mod mock;
+use mock::{
+	extra, fake_message_hash, new_test_ext_with_balances, new_test_ext_with_balances_and_assets,
+	DeliveryFees, ExistentialDeposit, HereLocation, RuntimeCall, RuntimeEvent, TestClient, TestXt,
+};
+
+// Scenario: User `1` in the local chain (id 2000) wants to transfer assets to account `[0u8; 32]`
+// on "AssetHub". He wants to make sure he has enough for fees, so before he calls the
+// `transfer_asset` extrinsic to do the transfer, he decides to use the `XcmDryRunApi` and
+// `XcmPaymentApi` runtime APIs to estimate fees. This uses a teleport because we're dealing with
+// the native token of the chain, which is registered on "AssetHub". The fees are sent as a reserve
+// asset transfer, since they're paid in the relay token.
+//
+//                 Teleport Parachain(2000) Token
+//                 Reserve Asset Transfer Relay Token for fees
+// Parachain(2000) -------------------------------------------> Parachain(1000)
+#[test]
+fn fee_estimation_for_teleport() {
+	let _ = env_logger::builder().is_test(true).try_init();
+	let who = 1; // AccountId = u64.
+	let balances = vec![(who, 100 + DeliveryFees::get() + ExistentialDeposit::get())];
+	let assets = vec![(1, who, 50)];
+	new_test_ext_with_balances_and_assets(balances, assets).execute_with(|| {
+		let client = TestClient;
+		let runtime_api = client.runtime_api();
+		let extrinsic = TestXt::new(
+			RuntimeCall::XcmPallet(pallet_xcm::Call::transfer_assets {
+				dest: Box::new(VersionedLocation::V4((Parent, Parachain(1000)).into())),
+				beneficiary: Box::new(VersionedLocation::V4(
+					AccountId32 { id: [0u8; 32], network: None }.into(),
+				)),
+				assets: Box::new(VersionedAssets::V4(
+					vec![(Here, 100u128).into(), (Parent, 20u128).into()].into(),
+				)),
+				fee_asset_item: 1, // Fees are paid with the RelayToken
+				weight_limit: Unlimited,
+			}),
+			Some((who, extra())),
+		);
+		let dry_run_effects =
+			runtime_api.dry_run_extrinsic(H256::zero(), extrinsic).unwrap().unwrap();
+
+		assert_eq!(
+			dry_run_effects.local_xcm,
+			Some(VersionedXcm::V4(
+				Xcm::builder_unsafe()
+					.withdraw_asset((Parent, 20u128))
+					.burn_asset((Parent, 20u128))
+					.withdraw_asset((Here, 100u128))
+					.burn_asset((Here, 100u128))
+					.build()
+			)),
+		);
+		let send_destination = Location::new(1, [Parachain(1000)]);
+		let send_message = Xcm::<()>::builder_unsafe()
+			.withdraw_asset((Parent, 20u128))
+			.buy_execution((Parent, 20u128), Unlimited)
+			.receive_teleported_asset(((Parent, Parachain(2000)), 100u128))
+			.clear_origin()
+			.deposit_asset(AllCounted(2), [0u8; 32])
+			.build();
+		assert_eq!(
+			dry_run_effects.forwarded_xcms,
+			vec![(
+				VersionedLocation::V4(send_destination.clone()),
+				vec![VersionedXcm::V4(send_message.clone())],
+			),],
+		);
+
+		assert_eq!(
+			dry_run_effects.emitted_events,
+			vec![
+				RuntimeEvent::System(frame_system::Event::NewAccount {
+					account: 8660274132218572653 // TODO: Why is this not `1`?
+				}),
+				RuntimeEvent::Balances(pallet_balances::Event::Endowed {
+					account: 8660274132218572653,
+					free_balance: 100
+				}),
+				RuntimeEvent::Balances(pallet_balances::Event::Minted {
+					who: 8660274132218572653,
+					amount: 100
+				}),
+				RuntimeEvent::AssetsPallet(pallet_assets::Event::Burned {
+					asset_id: 1,
+					owner: 1,
+					balance: 20
+				}),
+				RuntimeEvent::Balances(pallet_balances::Event::Burned { who: 1, amount: 100 }),
+				RuntimeEvent::XcmPallet(pallet_xcm::Event::Attempted {
+					outcome: Outcome::Complete { used: Weight::from_parts(400, 40) },
+				}),
+				RuntimeEvent::Balances(pallet_balances::Event::Burned { who: 1, amount: 20 }),
+				RuntimeEvent::XcmPallet(pallet_xcm::Event::FeesPaid {
+					paying: AccountIndex64 { index: 1, network: None }.into(),
+					fees: (Here, 20u128).into(),
+				}),
+				RuntimeEvent::XcmPallet(pallet_xcm::Event::Sent {
+					origin: AccountIndex64 { index: 1, network: None }.into(),
+					destination: (Parent, Parachain(1000)).into(),
+					message: send_message.clone(),
+					message_id: fake_message_hash(&send_message),
+				}),
+				RuntimeEvent::System(frame_system::Event::ExtrinsicSuccess {
+					dispatch_info: DispatchInfo {
+						weight: Weight::from_parts(107074070, 0), /* Will break if weights get
+						                                           * updated. */
+						class: DispatchClass::Normal,
+						pays_fee: Pays::Yes,
+					}
+				}),
+			]
+		);
+
+		// Weighing the local program is not relevant for extrinsics that already
+		// take this weight into account.
+		// In this case, we really only care about delivery fees.
+		let local_xcm = dry_run_effects.local_xcm.unwrap();
+
+		// We get a double result since the actual call returns a result and the runtime api returns
+		// results.
+		let weight =
+			runtime_api.query_xcm_weight(H256::zero(), local_xcm.clone()).unwrap().unwrap();
+		assert_eq!(weight, Weight::from_parts(400, 40));
+		let execution_fees = runtime_api
+			.query_weight_to_asset_fee(
+				H256::zero(),
+				weight,
+				VersionedAssetId::V4(HereLocation::get().into()),
+			)
+			.unwrap()
+			.unwrap();
+		assert_eq!(execution_fees, 440);
+
+		let mut forwarded_xcms_iter = dry_run_effects.forwarded_xcms.into_iter();
+
+		let (destination, remote_messages) = forwarded_xcms_iter.next().unwrap();
+		let remote_message = &remote_messages[0];
+
+		let delivery_fees = runtime_api
+			.query_delivery_fees(H256::zero(), destination.clone(), remote_message.clone())
+			.unwrap()
+			.unwrap();
+		assert_eq!(delivery_fees, VersionedAssets::V4((Here, 20u128).into()));
+
+		// This would have to be the runtime API of the destination,
+		// which we have the location for.
+		// If I had a mock runtime configured for "AssetHub" then I would use the
+		// runtime APIs from that.
+		let remote_execution_weight = runtime_api
+			.query_xcm_weight(H256::zero(), remote_message.clone())
+			.unwrap()
+			.unwrap();
+		let remote_execution_fees = runtime_api
+			.query_weight_to_asset_fee(
+				H256::zero(),
+				remote_execution_weight,
+				VersionedAssetId::V4(HereLocation::get().into()),
+			)
+			.unwrap()
+			.unwrap();
+		assert_eq!(remote_execution_fees, 550);
+
+		// Now we know that locally we need to use `execution_fees` and
+		// `delivery_fees`.
+		// On the message we forward to the destination, we need to
+		// put `remote_execution_fees` in `BuyExecution`.
+		// For the `transfer_assets` extrinsic, it just means passing the correct amount
+		// of fees in the parameters.
+	});
+}
+
+// Same scenario as in `fee_estimation_for_teleport`, but the user in parachain 2000 wants
+// to send relay tokens over to parachain 1000.
+//
+//                 Reserve Asset Transfer Relay Token
+//                 Reserve Asset Transfer Relay Token for fees
+// Parachain(2000) -------------------------------------------> Parachain(1000)
+#[test]
+fn dry_run_reserve_asset_transfer() {
+	let _ = env_logger::builder().is_test(true).try_init();
+	let who = 1; // AccountId = u64.
+			 // Native token used for fees.
+	let balances = vec![(who, DeliveryFees::get() + ExistentialDeposit::get())];
+	// Relay token is the one we want to transfer.
+	let assets = vec![(1, who, 100)]; // id, account_id, balance.
+	new_test_ext_with_balances_and_assets(balances, assets).execute_with(|| {
+		let client = TestClient;
+		let runtime_api = client.runtime_api();
+		let extrinsic = TestXt::new(
+			RuntimeCall::XcmPallet(pallet_xcm::Call::transfer_assets {
+				dest: Box::new(VersionedLocation::V4((Parent, Parachain(1000)).into())),
+				beneficiary: Box::new(VersionedLocation::V4(
+					AccountId32 { id: [0u8; 32], network: None }.into(),
+				)),
+				assets: Box::new(VersionedAssets::V4((Parent, 100u128).into())),
+				fee_asset_item: 0,
+				weight_limit: Unlimited,
+			}),
+			Some((who, extra())),
+		);
+		let dry_run_effects =
+			runtime_api.dry_run_extrinsic(H256::zero(), extrinsic).unwrap().unwrap();
+
+		assert_eq!(
+			dry_run_effects.local_xcm,
+			Some(VersionedXcm::V4(
+				Xcm::builder_unsafe()
+					.withdraw_asset((Parent, 100u128))
+					.burn_asset((Parent, 100u128))
+					.build()
+			)),
+		);
+
+		// In this case, the transfer type is `DestinationReserve`, so the remote xcm just withdraws
+		// the assets.
+		let send_destination = Location::new(1, Parachain(1000));
+		let send_message = Xcm::<()>::builder_unsafe()
+			.withdraw_asset((Parent, 100u128))
+			.clear_origin()
+			.buy_execution((Parent, 100u128), Unlimited)
+			.deposit_asset(AllCounted(1), [0u8; 32])
+			.build();
+		assert_eq!(
+			dry_run_effects.forwarded_xcms,
+			vec![(
+				VersionedLocation::V4(send_destination.clone()),
+				vec![VersionedXcm::V4(send_message.clone())],
+			),],
+		);
+
+		assert_eq!(
+			dry_run_effects.emitted_events,
+			vec![
+				RuntimeEvent::AssetsPallet(pallet_assets::Event::Burned {
+					asset_id: 1,
+					owner: 1,
+					balance: 100
+				}),
+				RuntimeEvent::XcmPallet(pallet_xcm::Event::Attempted {
+					outcome: Outcome::Complete { used: Weight::from_parts(200, 20) }
+				}),
+				RuntimeEvent::Balances(pallet_balances::Event::Burned { who: 1, amount: 20 }),
+				RuntimeEvent::XcmPallet(pallet_xcm::Event::FeesPaid {
+					paying: AccountIndex64 { index: 1, network: None }.into(),
+					fees: (Here, 20u128).into()
+				}),
+				RuntimeEvent::XcmPallet(pallet_xcm::Event::Sent {
+					origin: AccountIndex64 { index: 1, network: None }.into(),
+					destination: send_destination.clone(),
+					message: send_message.clone(),
+					message_id: fake_message_hash(&send_message),
+				}),
+				RuntimeEvent::System(frame_system::Event::ExtrinsicSuccess {
+					dispatch_info: DispatchInfo {
+						weight: Weight::from_parts(107074066, 0), /* Will break if weights get
+						                                           * updated. */
+						class: DispatchClass::Normal,
+						pays_fee: Pays::Yes,
+					}
+				}),
+			]
+		);
+	});
+}
+
+#[test]
+fn dry_run_xcm() {
+	let _ = env_logger::builder().is_test(true).try_init();
+	let who = 1; // AccountId = u64.
+	let transfer_amount = 100u128;
+	// We need to build the XCM to weigh it and then build the real XCM that can pay for fees.
+	let inner_xcm = Xcm::<()>::builder_unsafe()
+		.buy_execution((Here, 1u128), Unlimited) // We'd need to query the destination chain for fees.
+		.deposit_asset(AllCounted(1), [0u8; 32])
+		.build();
+	let xcm_to_weigh = Xcm::<RuntimeCall>::builder_unsafe()
+		.withdraw_asset((Here, transfer_amount))
+		.clear_origin()
+		.buy_execution((Here, transfer_amount), Unlimited)
+		.deposit_reserve_asset(AllCounted(1), (Parent, Parachain(2100)), inner_xcm.clone())
+		.build();
+	let client = TestClient;
+	let runtime_api = client.runtime_api();
+	let xcm_weight = runtime_api
+		.query_xcm_weight(H256::zero(), VersionedXcm::V4(xcm_to_weigh.clone().into()))
+		.unwrap()
+		.unwrap();
+	let execution_fees = runtime_api
+		.query_weight_to_asset_fee(H256::zero(), xcm_weight, VersionedAssetId::V4(Here.into()))
+		.unwrap()
+		.unwrap();
+	let xcm = Xcm::<RuntimeCall>::builder_unsafe()
+		.withdraw_asset((Here, transfer_amount + execution_fees))
+		.clear_origin()
+		.buy_execution((Here, execution_fees), Unlimited)
+		.deposit_reserve_asset(AllCounted(1), (Parent, Parachain(2100)), inner_xcm.clone())
+		.build();
+	let balances = vec![(
+		who,
+		transfer_amount + execution_fees + DeliveryFees::get() + ExistentialDeposit::get(),
+	)];
+	new_test_ext_with_balances(balances).execute_with(|| {
+		let dry_run_effects = runtime_api
+			.dry_run_xcm(
+				H256::zero(),
+				VersionedLocation::V4(AccountIndex64 { index: 1, network: None }.into()),
+				VersionedXcm::V4(xcm),
+			)
+			.unwrap()
+			.unwrap();
+		assert_eq!(
+			dry_run_effects.forwarded_xcms,
+			vec![(
+				VersionedLocation::V4((Parent, Parachain(2100)).into()),
+				vec![VersionedXcm::V4(
+					Xcm::<()>::builder_unsafe()
+						.reserve_asset_deposited((
+							(Parent, Parachain(2000)),
+							transfer_amount + execution_fees - DeliveryFees::get()
+						))
+						.clear_origin()
+						.buy_execution((Here, 1u128), Unlimited)
+						.deposit_asset(AllCounted(1), [0u8; 32])
+						.build()
+				)],
+			),]
+		);
+
+		assert_eq!(
+			dry_run_effects.emitted_events,
+			vec![
+				RuntimeEvent::Balances(pallet_balances::Event::Burned { who: 1, amount: 540 }),
+				RuntimeEvent::System(frame_system::Event::NewAccount { account: 2100 }),
+				RuntimeEvent::Balances(pallet_balances::Event::Endowed {
+					account: 2100,
+					free_balance: 520
+				}),
+				RuntimeEvent::Balances(pallet_balances::Event::Minted { who: 2100, amount: 520 }),
+			]
+		);
+	});
+}
diff --git a/polkadot/xcm/xcm-fee-payment-runtime-api/tests/mock.rs b/polkadot/xcm/xcm-fee-payment-runtime-api/tests/mock.rs
new file mode 100644
index 0000000000000000000000000000000000000000..d7b18d90a501815018fde21e25481872e33753a6
--- /dev/null
+++ b/polkadot/xcm/xcm-fee-payment-runtime-api/tests/mock.rs
@@ -0,0 +1,525 @@
+// Copyright (C) Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Substrate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// Substrate is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
+
+//! Mock runtime for tests.
+//! Implements both runtime APIs for fee estimation and getting the messages for transfers.
+
+use codec::Encode;
+use frame_support::{
+	construct_runtime, derive_impl, parameter_types,
+	traits::{
+		AsEnsureOriginWithArg, ConstU128, ConstU32, Contains, ContainsPair, Everything, Nothing,
+		OriginTrait,
+	},
+	weights::WeightToFee as WeightToFeeT,
+};
+use frame_system::{EnsureRoot, RawOrigin as SystemRawOrigin};
+use pallet_xcm::TestWeightInfo;
+use sp_runtime::{
+	traits::{Block as BlockT, Get, IdentityLookup, MaybeEquivalence, TryConvert},
+	BuildStorage, SaturatedConversion,
+};
+use sp_std::{cell::RefCell, marker::PhantomData};
+use xcm::{prelude::*, Version as XcmVersion};
+use xcm_builder::{
+	AllowTopLevelPaidExecutionFrom, ConvertedConcreteId, EnsureXcmOrigin, FixedRateOfFungible,
+	FixedWeightBounds, FungibleAdapter, FungiblesAdapter, IsConcrete, MintLocation, NoChecking,
+	TakeWeightCredit,
+};
+use xcm_executor::{
+	traits::{ConvertLocation, JustTry},
+	XcmExecutor,
+};
+
+use xcm_fee_payment_runtime_api::{
+	dry_run::{Error as XcmDryRunApiError, ExtrinsicDryRunEffects, XcmDryRunApi, XcmDryRunEffects},
+	fees::{Error as XcmPaymentApiError, XcmPaymentApi},
+};
+
+construct_runtime! {
+	pub enum TestRuntime {
+		System: frame_system,
+		Balances: pallet_balances,
+		AssetsPallet: pallet_assets,
+		XcmPallet: pallet_xcm,
+	}
+}
+
+pub type SignedExtra = (
+	// frame_system::CheckEra<TestRuntime>,
+	// frame_system::CheckNonce<TestRuntime>,
+	frame_system::CheckWeight<TestRuntime>,
+);
+pub type TestXt = sp_runtime::testing::TestXt<RuntimeCall, SignedExtra>;
+type Block = sp_runtime::testing::Block<TestXt>;
+type Balance = u128;
+type AssetIdForAssetsPallet = u32;
+type AccountId = u64;
+
+pub fn extra() -> SignedExtra {
+	(frame_system::CheckWeight::new(),)
+}
+
+type Executive = frame_executive::Executive<
+	TestRuntime,
+	Block,
+	frame_system::ChainContext<TestRuntime>,
+	TestRuntime,
+	AllPalletsWithSystem,
+	(),
+>;
+
+#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
+impl frame_system::Config for TestRuntime {
+	type Block = Block;
+	type AccountId = AccountId;
+	type AccountData = pallet_balances::AccountData<Balance>;
+	type Lookup = IdentityLookup<AccountId>;
+}
+
+#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)]
+impl pallet_balances::Config for TestRuntime {
+	type AccountStore = System;
+	type Balance = Balance;
+	type ExistentialDeposit = ExistentialDeposit;
+}
+
+#[derive_impl(pallet_assets::config_preludes::TestDefaultConfig)]
+impl pallet_assets::Config for TestRuntime {
+	type AssetId = AssetIdForAssetsPallet;
+	type Balance = Balance;
+	type Currency = Balances;
+	type CreateOrigin = AsEnsureOriginWithArg<frame_system::EnsureSigned<AccountId>>;
+	type ForceOrigin = frame_system::EnsureRoot<AccountId>;
+	type Freezer = ();
+	type AssetDeposit = ConstU128<1>;
+	type AssetAccountDeposit = ConstU128<10>;
+	type MetadataDepositBase = ConstU128<1>;
+	type MetadataDepositPerByte = ConstU128<1>;
+	type ApprovalDeposit = ConstU128<1>;
+	#[cfg(feature = "runtime-benchmarks")]
+	type BenchmarkHelper = ();
+}
+
+thread_local! {
+	pub static SENT_XCM: RefCell<Vec<(Location, Xcm<()>)>> = const { RefCell::new(Vec::new()) };
+}
+
+pub(crate) fn sent_xcm() -> Vec<(Location, Xcm<()>)> {
+	SENT_XCM.with(|q| (*q.borrow()).clone())
+}
+
+pub struct TestXcmSender;
+impl SendXcm for TestXcmSender {
+	type Ticket = (Location, Xcm<()>);
+	fn validate(
+		dest: &mut Option<Location>,
+		msg: &mut Option<Xcm<()>>,
+	) -> SendResult<Self::Ticket> {
+		let ticket = (dest.take().unwrap(), msg.take().unwrap());
+		let fees: Assets = (HereLocation::get(), DeliveryFees::get()).into();
+		Ok((ticket, fees))
+	}
+	fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
+		let hash = fake_message_hash(&ticket.1);
+		SENT_XCM.with(|q| q.borrow_mut().push(ticket));
+		Ok(hash)
+	}
+}
+
+pub(crate) fn fake_message_hash<Call>(message: &Xcm<Call>) -> XcmHash {
+	message.using_encoded(sp_io::hashing::blake2_256)
+}
+
+pub type XcmRouter = TestXcmSender;
+
+parameter_types! {
+	pub const DeliveryFees: u128 = 20; // Random value.
+	pub const ExistentialDeposit: u128 = 1; // Random value.
+	pub const BaseXcmWeight: Weight = Weight::from_parts(100, 10); // Random value.
+	pub const MaxInstructions: u32 = 100;
+	pub const NativeTokenPerSecondPerByte: (AssetId, u128, u128) = (AssetId(HereLocation::get()), 1, 1);
+	pub UniversalLocation: InteriorLocation = [GlobalConsensus(NetworkId::Westend), Parachain(2000)].into();
+	pub static AdvertisedXcmVersion: XcmVersion = 4;
+	pub const HereLocation: Location = Location::here();
+	pub const RelayLocation: Location = Location::parent();
+	pub const MaxAssetsIntoHolding: u32 = 64;
+	pub CheckAccount: AccountId = XcmPallet::check_account();
+	pub LocalCheckAccount: (AccountId, MintLocation) = (CheckAccount::get(), MintLocation::Local);
+	pub const AnyNetwork: Option<NetworkId> = None;
+}
+
+/// Simple `WeightToFee` implementation that adds the ref_time by the proof_size.
+pub struct WeightToFee;
+impl WeightToFeeT for WeightToFee {
+	type Balance = Balance;
+	fn weight_to_fee(weight: &Weight) -> Self::Balance {
+		Self::Balance::saturated_from(weight.ref_time())
+			.saturating_add(Self::Balance::saturated_from(weight.proof_size()))
+	}
+}
+
+type Weigher = FixedWeightBounds<BaseXcmWeight, RuntimeCall, MaxInstructions>;
+
+/// Matches the pair (NativeToken, AssetHub).
+/// This is used in the `IsTeleporter` configuration item, meaning we accept our native token
+/// coming from AssetHub as a teleport.
+pub struct NativeTokenToAssetHub;
+impl ContainsPair<Asset, Location> for NativeTokenToAssetHub {
+	fn contains(asset: &Asset, origin: &Location) -> bool {
+		matches!(asset.id.0.unpack(), (0, [])) && matches!(origin.unpack(), (1, [Parachain(1000)]))
+	}
+}
+
+/// Matches the pair (RelayToken, AssetHub).
+/// This is used in the `IsReserve` configuration item, meaning we accept the relay token
+/// coming from AssetHub as a reserve asset transfer.
+pub struct RelayTokenToAssetHub;
+impl ContainsPair<Asset, Location> for RelayTokenToAssetHub {
+	fn contains(asset: &Asset, origin: &Location) -> bool {
+		matches!(asset.id.0.unpack(), (1, [])) && matches!(origin.unpack(), (1, [Parachain(1000)]))
+	}
+}
+
+/// Converts locations that are only the `AccountIndex64` junction into local u64 accounts.
+pub struct AccountIndex64Aliases<Network, AccountId>(PhantomData<(Network, AccountId)>);
+impl<Network: Get<Option<NetworkId>>, AccountId: From<u64>> ConvertLocation<AccountId>
+	for AccountIndex64Aliases<Network, AccountId>
+{
+	fn convert_location(location: &Location) -> Option<AccountId> {
+		let index = match location.unpack() {
+			(0, [AccountIndex64 { index, network: None }]) => index,
+			(0, [AccountIndex64 { index, network }]) if *network == Network::get() => index,
+			_ => return None,
+		};
+		Some((*index).into())
+	}
+}
+
+/// Custom location converter to turn sibling chains into u64 accounts.
+pub struct SiblingChainToIndex64;
+impl ConvertLocation<AccountId> for SiblingChainToIndex64 {
+	fn convert_location(location: &Location) -> Option<AccountId> {
+		let index = match location.unpack() {
+			(1, [Parachain(id)]) => id,
+			_ => return None,
+		};
+		Some((*index).into())
+	}
+}
+
+/// We alias local account locations to actual local accounts.
+/// We also allow sovereign accounts for other sibling chains.
+pub type LocationToAccountId = (AccountIndex64Aliases<AnyNetwork, u64>, SiblingChainToIndex64);
+
+pub type NativeTokenTransactor = FungibleAdapter<
+	// We use pallet-balances for handling this fungible asset.
+	Balances,
+	// The fungible asset handled by this transactor is the native token of the chain.
+	IsConcrete<HereLocation>,
+	// How we convert locations to accounts.
+	LocationToAccountId,
+	// We need to specify the AccountId type.
+	AccountId,
+	// We mint the native tokens locally, so we track how many we've sent away via teleports.
+	LocalCheckAccount,
+>;
+
+pub struct LocationToAssetIdForAssetsPallet;
+impl MaybeEquivalence<Location, AssetIdForAssetsPallet> for LocationToAssetIdForAssetsPallet {
+	fn convert(location: &Location) -> Option<AssetIdForAssetsPallet> {
+		match location.unpack() {
+			(1, []) => Some(1 as AssetIdForAssetsPallet),
+			_ => None,
+		}
+	}
+
+	fn convert_back(id: &AssetIdForAssetsPallet) -> Option<Location> {
+		match id {
+			1 => Some(Location::new(1, [])),
+			_ => None,
+		}
+	}
+}
+
+/// AssetTransactor for handling the relay chain token.
+pub type RelayTokenTransactor = FungiblesAdapter<
+	// We use pallet-assets for handling the relay token.
+	AssetsPallet,
+	// Matches the relay token.
+	ConvertedConcreteId<AssetIdForAssetsPallet, Balance, LocationToAssetIdForAssetsPallet, JustTry>,
+	// How we convert locations to accounts.
+	LocationToAccountId,
+	// We need to specify the AccountId type.
+	AccountId,
+	// We don't track teleports.
+	NoChecking,
+	(),
+>;
+
+pub type AssetTransactors = (NativeTokenTransactor, RelayTokenTransactor);
+
+pub struct HereAndInnerLocations;
+impl Contains<Location> for HereAndInnerLocations {
+	fn contains(location: &Location) -> bool {
+		matches!(location.unpack(), (0, []) | (0, _))
+	}
+}
+
+pub type Barrier = (
+	TakeWeightCredit, // We need this for pallet-xcm's extrinsics to work.
+	AllowTopLevelPaidExecutionFrom<HereAndInnerLocations>, /* TODO: Technically, we should allow
+	                   * messages from "AssetHub". */
+);
+
+pub type Trader = FixedRateOfFungible<NativeTokenPerSecondPerByte, ()>;
+
+pub struct XcmConfig;
+impl xcm_executor::Config for XcmConfig {
+	type RuntimeCall = RuntimeCall;
+	type XcmSender = XcmRouter;
+	type AssetTransactor = AssetTransactors;
+	type OriginConverter = ();
+	type IsReserve = RelayTokenToAssetHub;
+	type IsTeleporter = NativeTokenToAssetHub;
+	type UniversalLocation = UniversalLocation;
+	type Barrier = Barrier;
+	type Weigher = Weigher;
+	type Trader = Trader;
+	type ResponseHandler = ();
+	type AssetTrap = ();
+	type AssetLocker = ();
+	type AssetExchanger = ();
+	type AssetClaims = ();
+	type SubscriptionService = ();
+	type PalletInstancesInfo = AllPalletsWithSystem;
+	type MaxAssetsIntoHolding = MaxAssetsIntoHolding;
+	type FeeManager = ();
+	type MessageExporter = ();
+	type UniversalAliases = ();
+	type CallDispatcher = RuntimeCall;
+	type SafeCallFilter = Nothing;
+	type Aliasers = Nothing;
+	type TransactionalProcessor = ();
+	type HrmpNewChannelOpenRequestHandler = ();
+	type HrmpChannelAcceptedHandler = ();
+	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = XcmPallet;
+}
+
+/// Converts a signed origin of a u64 account into a location with only the `AccountIndex64`
+/// junction.
+pub struct SignedToAccountIndex64<RuntimeOrigin, AccountId>(
+	PhantomData<(RuntimeOrigin, AccountId)>,
+);
+impl<RuntimeOrigin: OriginTrait + Clone, AccountId: Into<u64>> TryConvert<RuntimeOrigin, Location>
+	for SignedToAccountIndex64<RuntimeOrigin, AccountId>
+where
+	RuntimeOrigin::PalletsOrigin: From<SystemRawOrigin<AccountId>>
+		+ TryInto<SystemRawOrigin<AccountId>, Error = RuntimeOrigin::PalletsOrigin>,
+{
+	fn try_convert(origin: RuntimeOrigin) -> Result<Location, RuntimeOrigin> {
+		origin.try_with_caller(|caller| match caller.try_into() {
+			Ok(SystemRawOrigin::Signed(who)) =>
+				Ok(Junction::AccountIndex64 { network: None, index: who.into() }.into()),
+			Ok(other) => Err(other.into()),
+			Err(other) => Err(other),
+		})
+	}
+}
+
+pub type LocalOriginToLocation = SignedToAccountIndex64<RuntimeOrigin, AccountId>;
+
+impl pallet_xcm::Config for TestRuntime {
+	type RuntimeEvent = RuntimeEvent;
+	type SendXcmOrigin = EnsureXcmOrigin<RuntimeOrigin, ()>;
+	type XcmRouter = XcmRouter;
+	type ExecuteXcmOrigin = EnsureXcmOrigin<RuntimeOrigin, LocalOriginToLocation>;
+	type XcmExecuteFilter = Nothing;
+	type XcmExecutor = XcmExecutor<XcmConfig>;
+	type XcmTeleportFilter = Everything; // Put everything instead of something more restricted.
+	type XcmReserveTransferFilter = Everything; // Same.
+	type Weigher = Weigher;
+	type UniversalLocation = UniversalLocation;
+	type RuntimeOrigin = RuntimeOrigin;
+	type RuntimeCall = RuntimeCall;
+	const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100;
+	type AdvertisedXcmVersion = AdvertisedXcmVersion;
+	type AdminOrigin = EnsureRoot<AccountId>;
+	type TrustedLockers = ();
+	type SovereignAccountOf = ();
+	type Currency = Balances;
+	type CurrencyMatcher = IsConcrete<HereLocation>;
+	type MaxLockers = ConstU32<0>;
+	type MaxRemoteLockConsumers = ConstU32<0>;
+	type RemoteLockConsumerIdentifier = ();
+	type WeightInfo = TestWeightInfo;
+}
+
+pub fn new_test_ext_with_balances(balances: Vec<(AccountId, Balance)>) -> sp_io::TestExternalities {
+	let mut t = frame_system::GenesisConfig::<TestRuntime>::default().build_storage().unwrap();
+
+	pallet_balances::GenesisConfig::<TestRuntime> { balances }
+		.assimilate_storage(&mut t)
+		.unwrap();
+
+	let mut ext = sp_io::TestExternalities::new(t);
+	ext.execute_with(|| System::set_block_number(1));
+	ext
+}
+
+pub fn new_test_ext_with_balances_and_assets(
+	balances: Vec<(AccountId, Balance)>,
+	assets: Vec<(AssetIdForAssetsPallet, AccountId, Balance)>,
+) -> sp_io::TestExternalities {
+	let mut t = frame_system::GenesisConfig::<TestRuntime>::default().build_storage().unwrap();
+
+	pallet_balances::GenesisConfig::<TestRuntime> { balances }
+		.assimilate_storage(&mut t)
+		.unwrap();
+
+	pallet_assets::GenesisConfig::<TestRuntime> {
+		assets: vec![
+			// id, owner, is_sufficient, min_balance.
+			// We don't actually need this to be sufficient, since we use the native assets in
+			// tests for the existential deposit.
+			(1, 0, true, 1),
+		],
+		metadata: vec![
+			// id, name, symbol, decimals.
+			(1, "Relay Token".into(), "RLY".into(), 12),
+		],
+		accounts: assets,
+	}
+	.assimilate_storage(&mut t)
+	.unwrap();
+
+	let mut ext = sp_io::TestExternalities::new(t);
+	ext.execute_with(|| System::set_block_number(1));
+	ext
+}
+
+#[derive(Clone)]
+pub(crate) struct TestClient;
+
+pub(crate) struct RuntimeApi {
+	_inner: TestClient,
+}
+
+impl sp_api::ProvideRuntimeApi<Block> for TestClient {
+	type Api = RuntimeApi;
+	fn runtime_api(&self) -> sp_api::ApiRef<Self::Api> {
+		RuntimeApi { _inner: self.clone() }.into()
+	}
+}
+
+sp_api::mock_impl_runtime_apis! {
+	impl XcmPaymentApi<Block> for RuntimeApi {
+		fn query_acceptable_payment_assets(xcm_version: XcmVersion) -> Result<Vec<VersionedAssetId>, XcmPaymentApiError> {
+			if xcm_version != 4 { return Err(XcmPaymentApiError::UnhandledXcmVersion) };
+			Ok(vec![VersionedAssetId::V4(HereLocation::get().into())])
+		}
+
+		fn query_xcm_weight(message: VersionedXcm<()>) -> Result<Weight, XcmPaymentApiError> {
+			XcmPallet::query_xcm_weight(message)
+		}
+
+		fn query_weight_to_asset_fee(weight: Weight, asset: VersionedAssetId) -> Result<u128, XcmPaymentApiError> {
+			let local_asset = VersionedAssetId::V4(HereLocation::get().into());
+			let asset = asset
+				.into_version(4)
+				.map_err(|_| XcmPaymentApiError::VersionedConversionFailed)?;
+
+			if asset != local_asset { return Err(XcmPaymentApiError::AssetNotFound); }
+
+			Ok(WeightToFee::weight_to_fee(&weight))
+		}
+
+		fn query_delivery_fees(destination: VersionedLocation, message: VersionedXcm<()>) -> Result<VersionedAssets, XcmPaymentApiError> {
+			XcmPallet::query_delivery_fees(destination, message)
+		}
+	}
+
+	impl XcmDryRunApi<Block, RuntimeCall, RuntimeEvent> for RuntimeApi {
+		fn dry_run_extrinsic(extrinsic: <Block as BlockT>::Extrinsic) -> Result<ExtrinsicDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			use xcm_executor::RecordXcm;
+			// We want to record the XCM that's executed, so we can return it.
+			pallet_xcm::Pallet::<TestRuntime>::set_record_xcm(true);
+			let result = Executive::apply_extrinsic(extrinsic).map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_extrinsic",
+					"Applying extrinsic failed with error {:?}",
+					error,
+				);
+				XcmDryRunApiError::InvalidExtrinsic
+			})?;
+			// Nothing gets committed to storage in runtime APIs, so there's no harm in leaving the flag as true.
+			let local_xcm = pallet_xcm::Pallet::<TestRuntime>::recorded_xcm();
+			let forwarded_xcms = sent_xcm()
+				.into_iter()
+				.map(|(location, message)| (
+					VersionedLocation::V4(location),
+					vec![VersionedXcm::V4(message)],
+				)).collect();
+			let events: Vec<RuntimeEvent> = System::events().iter().map(|record| record.event.clone()).collect();
+			Ok(ExtrinsicDryRunEffects {
+				local_xcm: local_xcm.map(VersionedXcm::<()>::V4),
+				forwarded_xcms,
+				emitted_events: events,
+				execution_result: result,
+			})
+		}
+
+		fn dry_run_xcm(origin_location: VersionedLocation, xcm: VersionedXcm<RuntimeCall>) -> Result<XcmDryRunEffects<RuntimeEvent>, XcmDryRunApiError> {
+			let origin_location: Location = origin_location.try_into().map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					"Location version conversion failed with error: {:?}",
+					error,
+				);
+				XcmDryRunApiError::VersionedConversionFailed
+			})?;
+			let xcm: Xcm<RuntimeCall> = xcm.try_into().map_err(|error| {
+				log::error!(
+					target: "xcm::XcmDryRunApi::dry_run_xcm",
+					"Xcm version conversion failed with error {:?}",
+					error,
+				);
+				XcmDryRunApiError::VersionedConversionFailed
+			})?;
+			let mut hash = fake_message_hash(&xcm);
+			let result = XcmExecutor::<XcmConfig>::prepare_and_execute(
+				origin_location,
+				xcm,
+				&mut hash,
+				Weight::MAX, // Max limit available for execution.
+				Weight::zero(),
+			);
+			let forwarded_xcms = sent_xcm()
+				.into_iter()
+				.map(|(location, message)| (
+					VersionedLocation::V4(location),
+					vec![VersionedXcm::V4(message)],
+				)).collect();
+			let events: Vec<RuntimeEvent> = System::events().iter().map(|record| record.event.clone()).collect();
+			Ok(XcmDryRunEffects {
+				forwarded_xcms,
+				emitted_events: events,
+				execution_result: result,
+			})
+		}
+	}
+}
diff --git a/polkadot/xcm/xcm-simulator/example/src/parachain/xcm_config/mod.rs b/polkadot/xcm/xcm-simulator/example/src/parachain/xcm_config/mod.rs
index 0ba02aab9bf92ac699485fcd5803e2c658574d13..a6b55d1bd9be05641db343f2f301ae560e0fea29 100644
--- a/polkadot/xcm/xcm-simulator/example/src/parachain/xcm_config/mod.rs
+++ b/polkadot/xcm/xcm-simulator/example/src/parachain/xcm_config/mod.rs
@@ -60,4 +60,5 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
diff --git a/polkadot/xcm/xcm-simulator/example/src/relay_chain/xcm_config/mod.rs b/polkadot/xcm/xcm-simulator/example/src/relay_chain/xcm_config/mod.rs
index a7a8bae51567b3a526333fc56e14ff38c337c776..c5d5fa66732b939b9a03e1da2cf9c658c4e61acb 100644
--- a/polkadot/xcm/xcm-simulator/example/src/relay_chain/xcm_config/mod.rs
+++ b/polkadot/xcm/xcm-simulator/example/src/relay_chain/xcm_config/mod.rs
@@ -59,4 +59,5 @@ impl Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = XcmPallet;
 }
diff --git a/polkadot/xcm/xcm-simulator/fuzzer/src/parachain.rs b/polkadot/xcm/xcm-simulator/fuzzer/src/parachain.rs
index d8d65fbf0ce74a17b75e6bff8c9acfc02dbf928c..502bcca2d44270263a45eeaf305b924d8b37c509 100644
--- a/polkadot/xcm/xcm-simulator/fuzzer/src/parachain.rs
+++ b/polkadot/xcm/xcm-simulator/fuzzer/src/parachain.rs
@@ -159,6 +159,7 @@ impl Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = ();
 }
 
 #[frame_support::pallet]
diff --git a/polkadot/xcm/xcm-simulator/fuzzer/src/relay_chain.rs b/polkadot/xcm/xcm-simulator/fuzzer/src/relay_chain.rs
index cf3ca0de2bb46a8abe6f5fcd28dbf19ce5faf41b..4740aee83d870a0e260512ac15a982c515a1e803 100644
--- a/polkadot/xcm/xcm-simulator/fuzzer/src/relay_chain.rs
+++ b/polkadot/xcm/xcm-simulator/fuzzer/src/relay_chain.rs
@@ -160,6 +160,7 @@ impl Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = ();
 }
 
 pub type LocalOriginToLocation = SignedToAccountId32<RuntimeOrigin, AccountId, ThisNetwork>;
diff --git a/prdoc/pr_3872.prdoc b/prdoc/pr_3872.prdoc
new file mode 100644
index 0000000000000000000000000000000000000000..3a5be3d2bc74d08ec83aa1cacaceb294e948c563
--- /dev/null
+++ b/prdoc/pr_3872.prdoc
@@ -0,0 +1,86 @@
+title: XcmDryRunApi - Runtime API for dry-running extrinsics and XCM programs.
+
+doc:
+  - audience: Runtime Dev
+    description: |
+      This PR introduces a new runtime API, the XcmDryRunApi, that allows dry-running
+      extrinsics and XCM programs to get their execution effects.
+      These effects include:
+      - Local execution result, either pass or fail
+      - Emitted events
+      - Forwarded XCMs
+      - In the case of extrinsics, the XCM program that they execute
+      This API can be used on its own to test extrinsics or XCM programs,
+      or used alongside the XcmPaymentApi to estimate execution and delivery
+      fees.
+
+      This PR also adds a new configuration item to XCM: XcmRecorder.
+      This can be set to either (), the xcm pallet, or some custom implementation.
+      If set to (), the dry run API will not return the local XCM program executed
+      by running an extrinsic.
+      After this PR, it is necessary to add the new configuration item to your xcm
+      configs.
+  - audience: Runtime User
+    description: |
+      This PR introduces a new runtime API, the XcmDryRunApi, that allows dry-running
+      extrinsics and XCM programs to get their execution effects.
+      These effects include:
+      - Local execution result, either pass or fail
+      - Emitted events
+      - Forwarded XCMs
+      - In the case of extrinsics, the XCM program that they execute
+      This API can be used on its own to test extrinsics or XCM programs,
+      or used alongside the XcmPaymentApi to estimate execution and delivery
+      fees.
+
+crates:
+  - name: xcm-fee-payment-runtime-api
+    bump: major
+  - name: pallet-xcm
+    bump: minor
+  - name: staging-xcm-executor
+    bump: minor
+  - name: staging-xcm-builder
+    bump: minor
+  - name: rococo-runtime
+    bump: minor
+  - name: westend-runtime
+    bump: minor
+  - name: pallet-xcm-benchmarks
+    bump: minor
+  - name: asset-hub-rococo-runtime
+    bump: minor
+  - name: asset-hub-westend-runtime
+    bump: minor
+  - name: bridge-hub-rococo-runtime
+    bump: minor
+  - name: bridge-hub-westend-runtime
+    bump: minor
+  - name: collectives-westend-runtime
+    bump: minor
+  - name: contracts-rococo-runtime
+    bump: minor
+  - name: coretime-rococo-runtime
+    bump: minor
+  - name: coretime-westend-runtime
+    bump: minor
+  - name: glutton-westend-runtime
+    bump: minor
+  - name: people-rococo-runtime
+    bump: minor
+  - name: people-westend-runtime
+    bump: minor
+  - name: shell-runtime
+    bump: minor
+  - name: penpal-runtime
+    bump: minor
+  - name: rococo-parachain-runtime
+    bump: minor
+  - name: polkadot-service
+    bump: minor
+  - name: polkadot-test-runtime
+    bump: minor
+  - name: parachain-template-runtime
+    bump: minor
+  - name: pallet-contracts-mock-network
+    bump: minor
diff --git a/substrate/frame/contracts/mock-network/src/parachain.rs b/substrate/frame/contracts/mock-network/src/parachain.rs
index 843efab1502e7304991682d1f13180ee28288fe2..b46d7df6c2bcf1f99a48540d5f1c5ef104d09206 100644
--- a/substrate/frame/contracts/mock-network/src/parachain.rs
+++ b/substrate/frame/contracts/mock-network/src/parachain.rs
@@ -285,6 +285,7 @@ impl Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 impl mock_msg_queue::Config for Runtime {
diff --git a/substrate/frame/contracts/mock-network/src/relay_chain.rs b/substrate/frame/contracts/mock-network/src/relay_chain.rs
index d5e0ec9c83fa73cc88f567f573f0d2172af23a7a..36a7de499ba99311889980ba2c02d8502602b945 100644
--- a/substrate/frame/contracts/mock-network/src/relay_chain.rs
+++ b/substrate/frame/contracts/mock-network/src/relay_chain.rs
@@ -185,6 +185,7 @@ impl Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = XcmPallet;
 }
 
 pub type LocalOriginToLocation = SignedToAccountId32<RuntimeOrigin, AccountId, RelayNetwork>;
diff --git a/templates/parachain/runtime/src/configs/xcm_config.rs b/templates/parachain/runtime/src/configs/xcm_config.rs
index c6b6e8da1b89d38a8f5f7695809972d35d83cf12..e162bcbf88686c00ea10dc9b06c985329faa763b 100644
--- a/templates/parachain/runtime/src/configs/xcm_config.rs
+++ b/templates/parachain/runtime/src/configs/xcm_config.rs
@@ -142,6 +142,7 @@ impl xcm_executor::Config for XcmConfig {
 	type HrmpNewChannelOpenRequestHandler = ();
 	type HrmpChannelAcceptedHandler = ();
 	type HrmpChannelClosingHandler = ();
+	type XcmRecorder = PolkadotXcm;
 }
 
 /// No local origins on this chain are allowed to dispatch XCM sends/executions.