diff --git a/Cargo.toml b/Cargo.toml
index d4a87b2a2..aa43bca67 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,23 +28,23 @@ panic = 'abort'     # Abort on panic
 default = []
 
 [dependencies]
-lightning = { version = "0.0.123", features = ["std"] }
-lightning-invoice = { version = "0.31.0" }
-lightning-net-tokio = { version = "0.0.123" }
-lightning-persister = { version = "0.0.123" }
-lightning-background-processor = { version = "0.0.123", features = ["futures"] }
-lightning-rapid-gossip-sync = { version = "0.0.123" }
-lightning-transaction-sync = { version = "0.0.123", features = ["esplora-async-https", "time"] }
-lightning-liquidity = { version = "0.1.0-alpha.4", features = ["std"] }
-
-#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std"] }
-#lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" }
-#lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" }
-#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" }
-#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["futures"] }
-#lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main" }
-#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["esplora-async"] }
-#lightning-liquidity = { git = "https://github.com/lightningdevkit/lightning-liquidity", branch="main", features = ["std"] }
+# lightning = { version = "0.0.123", features = ["std"] }
+# lightning-invoice = { version = "0.31.0" }
+# lightning-net-tokio = { version = "0.0.123" }
+# lightning-persister = { version = "0.0.123" }
+# lightning-background-processor = { version = "0.0.123", features = ["futures"] }
+# lightning-rapid-gossip-sync = { version = "0.0.123" }
+# lightning-transaction-sync = { version = "0.0.123", features = ["esplora-async-https", "time"] }
+# lightning-liquidity = { version = "0.1.0-alpha.4", features = ["std"] }
+
+lightning = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["std"] }
+lightning-invoice = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" }
+lightning-net-tokio = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" }
+lightning-persister = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" }
+lightning-background-processor = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["futures"] }
+lightning-rapid-gossip-sync = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event" }
+lightning-transaction-sync = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["esplora-async"] }
+lightning-liquidity = { git = "https://github.com/jbesraa/lightning-liquidity", branch="pj-fixes", features = ["std"] }
 
 #lightning = { path = "../rust-lightning/lightning", features = ["std"] }
 #lightning-invoice = { path = "../rust-lightning/lightning-invoice" }
@@ -68,6 +68,7 @@ tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thr
 esplora-client = { version = "0.6", default-features = false }
 libc = "0.2"
 uniffi = { version = "0.26.0", features = ["build"], optional = true }
+payjoin = { version = "0.16.0", default-features = false, features = ["send", "receive", "v2"] }
 
 [target.'cfg(vss)'.dependencies]
 vss-client = "0.2"
@@ -77,8 +78,8 @@ prost = { version = "0.11.6", default-features = false}
 winapi = { version = "0.3", features = ["winbase"] }
 
 [dev-dependencies]
-lightning = { version = "0.0.123", features = ["std", "_test_utils"] }
-#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] }
+# lightning = { version = "0.0.123", features = ["std", "_test_utils"] }
+lightning = { git = "https://github.com/jbesraa/rust-lightning.git", branch="0.0.123-with-funding-brodsafe-event", features = ["std", "_test_utils"] }
 electrum-client = { version = "0.15.1", default-features = true }
 bitcoincore-rpc = { version = "0.17.0", default-features = false }
 proptest = "1.0.0"
@@ -101,3 +102,8 @@ panic = "abort"
 
 [profile.dev]
 panic = "abort"
+
+[[example]]
+name = "ldk-node-with-payjoin-support"
+path = "examples/ldk-node-with-payjoin-support.rs"
+
diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl
index 2723db573..f08c1161e 100644
--- a/bindings/ldk_node.udl
+++ b/bindings/ldk_node.udl
@@ -63,6 +63,7 @@ interface Node {
 	Bolt12Payment bolt12_payment();
 	SpontaneousPayment spontaneous_payment();
 	OnchainPayment onchain_payment();
+	PayjoinPayment payjoin_payment();
 	[Throws=NodeError]
 	void connect(PublicKey node_id, SocketAddress address, boolean persist);
 	[Throws=NodeError]
@@ -148,6 +149,13 @@ interface OnchainPayment {
 	Txid send_all_to_address([ByRef]Address address);
 };
 
+interface PayjoinPayment {
+	[Throws=NodeError]
+	void send(string payjoin_uri);
+	[Throws=NodeError]
+	void send_with_amount(string payjoin_uri, u64 amount_sats);
+};
+
 [Error]
 enum NodeError {
 	"AlreadyRunning",
@@ -196,6 +204,14 @@ enum NodeError {
 	"InsufficientFunds",
 	"LiquiditySourceUnavailable",
 	"LiquidityFeeTooHigh",
+	"PayjoinUnavailable",
+	"PayjoinUriInvalid",
+	"PayjoinRequestMissingAmount",
+	"PayjoinRequestCreationFailed",
+	"PayjoinResponseProcessingFailed",
+	"PayjoinReceiverUnavailable",
+	"PayjoinReceiverRequestValidationFailed",
+	"PayjoinReceiverEnrollementFailed"
 };
 
 dictionary NodeStatus {
@@ -227,6 +243,7 @@ enum BuildError {
 	"KVStoreSetupFailed",
 	"WalletSetupFailed",
 	"LoggerSetupFailed",
+	"InvalidPayjoinConfig",
 };
 
 [Enum]
@@ -238,6 +255,9 @@ interface Event {
 	ChannelPending(ChannelId channel_id, UserChannelId user_channel_id, ChannelId former_temporary_channel_id, PublicKey counterparty_node_id, OutPoint funding_txo);
 	ChannelReady(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id);
 	ChannelClosed(ChannelId channel_id, UserChannelId user_channel_id, PublicKey? counterparty_node_id, ClosureReason? reason);
+	PayjoinPaymentPending(Txid txid, u64 amount, ScriptBuf receipient);
+	PayjoinPaymentSuccess(Txid txid, u64 amount, ScriptBuf receipient);
+	PayjoinPaymentFailed(Txid? txid, u64 amount, ScriptBuf receipient, PayjoinPaymentFailureReason reason);
 };
 
 enum PaymentFailureReason {
@@ -249,6 +269,13 @@ enum PaymentFailureReason {
 	"UnexpectedError",
 };
 
+enum PayjoinPaymentFailureReason {
+	"Timeout",
+	"TransactionFinalisationFailed",
+	"InvalidReceiverResponse",
+};
+
+
 [Enum]
 interface ClosureReason {
 	CounterpartyForceClosed(UntrustedString peer_msg);
@@ -274,6 +301,7 @@ interface PaymentKind {
 	Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id);
 	Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret);
 	Spontaneous(PaymentHash hash, PaymentPreimage? preimage);
+	Payjoin();
 };
 
 enum PaymentDirection {
@@ -499,3 +527,6 @@ typedef string Mnemonic;
 
 [Custom]
 typedef string UntrustedString;
+
+[Custom]
+typedef string ScriptBuf;
diff --git a/examples/ldk-node-with-payjoin-support.rs b/examples/ldk-node-with-payjoin-support.rs
new file mode 100644
index 000000000..627f172b6
--- /dev/null
+++ b/examples/ldk-node-with-payjoin-support.rs
@@ -0,0 +1,71 @@
+use ldk_node::bitcoin::Network;
+use ldk_node::{Builder, LogLevel};
+
+fn main() {
+	let mut builder = Builder::new();
+	builder.set_log_level(LogLevel::Gossip);
+	builder.set_network(Network::Testnet);
+	builder.set_esplora_server("https://blockstream.info/testnet/api".to_string());
+	builder.set_gossip_source_rgs(
+		"https://rapidsync.lightningdevkit.org/testnet/snapshot".to_string(),
+	);
+
+	// Payjoin directory is needed only if you are setting up Payjoin receiver,
+	// not required for Payjoin sender.
+	let payjoin_directory = "https://payjo.in".to_string();
+	// Payjoin relay is required for both Payjoin receiver and sender.
+	let payjoin_relay = "https://pj.bobspacebkk.com".to_string();
+
+	// Enable sending payjoin transactions
+	// builder.set_payjoin_sender_config(payjoin_relay.clone());
+	// ohttp keys refer to the Payjoin directory keys that are needed for the Payjoin receiver
+	// enrollement. If those keys are not provided the node will attempt to fetch them for you.
+	// let ohttp_keys = None;
+	// Enable receiving payjoin transactions
+	builder.set_payjoin_config(payjoin_directory, payjoin_relay);
+
+	let node = builder.build().unwrap();
+
+	node.start().unwrap();
+
+	// Receiving payjoin transaction
+	let payjoin_payment = node.payjoin_payment();
+	let amount_to_receive = bitcoin::Amount::from_sat(1000);
+	let payjoin_uri = payjoin_payment.receive(amount_to_receive).unwrap();
+	let payjoin_uri = payjoin_uri.to_string();
+
+	println!("Payjoin URI: {}", payjoin_uri);
+
+	//** Open a channel from incoming payjoin transactions ***//
+	// let payjoin_payment = node.payjoin_payment();
+	// let channel_amount_sats = bitcoin::Amount::from_sat(10000);
+	// use bitcoin::secp256k1::PublicKey;
+	// use lightning::ln::msgs::SocketAddress;
+	// let counterparty_node_id: PublicKey = unimplemented!();
+	// let counterparty_address: SocketAddress = unimplemented!();
+	// let payjoin_uri = match payjoin_payment.receive_with_channel_opening(channel_amount_sats, None, true,
+	// 	counterparty_node_id, counterparty_address,
+	// ).await {
+	// 	Ok(a) => a,
+	// 	Err(e) => {
+	// 		panic!("{}", e);
+	// 	},
+	// };
+	// let payjoin_uri = payjoin_uri.to_string();
+	// println!("Payjoin URI: {}", payjoin_uri);
+
+	//** Sending payjoin transaction **//
+	// let payjoin_uri = payjoin::Uri::try_from(payjoin_uri).unwrap();
+	// match payjoin_payment.send(payjoin_uri, None, None).await {
+	// 	Ok(Some(txid)) => {
+	//		dbg!("Sent transaction and got a response. Transaction completed")
+	// 	},
+	// 	Ok(None) => {
+	//		dbg!("Sent transaction and got no response. We will keep polling the response for the next 24hours")
+	// 	},
+	// 	Err(e) => {
+	// 		dbg!(e);
+	// 	}
+	// }
+	node.stop().unwrap();
+}
diff --git a/src/builder.rs b/src/builder.rs
index a2a93aa79..ce456d4c2 100644
--- a/src/builder.rs
+++ b/src/builder.rs
@@ -11,6 +11,8 @@ use crate::io::sqlite_store::SqliteStore;
 use crate::liquidity::LiquiditySource;
 use crate::logger::{log_error, log_info, FilesystemLogger, Logger};
 use crate::message_handler::NodeCustomMessageHandler;
+use crate::payjoin_receiver::PayjoinReceiver;
+use crate::payment::payjoin::handler::PayjoinHandler;
 use crate::payment::store::PaymentStore;
 use crate::peer_store::PeerStore;
 use crate::tx_broadcaster::TransactionBroadcaster;
@@ -93,6 +95,13 @@ struct LiquiditySourceConfig {
 	lsps2_service: Option<(SocketAddress, PublicKey, Option<String>)>,
 }
 
+#[derive(Debug, Clone)]
+struct PayjoinConfig {
+	payjoin_directory: payjoin::Url,
+	payjoin_relay: payjoin::Url,
+	ohttp_keys: Option<payjoin::OhttpKeys>,
+}
+
 impl Default for LiquiditySourceConfig {
 	fn default() -> Self {
 		Self { lsps2_service: None }
@@ -132,6 +141,8 @@ pub enum BuildError {
 	WalletSetupFailed,
 	/// We failed to setup the logger.
 	LoggerSetupFailed,
+	/// Invalid Payjoin configuration.
+	InvalidPayjoinConfig,
 }
 
 impl fmt::Display for BuildError {
@@ -152,6 +163,10 @@ impl fmt::Display for BuildError {
 			Self::KVStoreSetupFailed => write!(f, "Failed to setup KVStore."),
 			Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."),
 			Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."),
+			Self::InvalidPayjoinConfig => write!(
+				f,
+				"Invalid Payjoin configuration. Make sure the provided arguments are valid URLs."
+			),
 		}
 	}
 }
@@ -172,6 +187,7 @@ pub struct NodeBuilder {
 	chain_data_source_config: Option<ChainDataSourceConfig>,
 	gossip_source_config: Option<GossipSourceConfig>,
 	liquidity_source_config: Option<LiquiditySourceConfig>,
+	payjoin_config: Option<PayjoinConfig>,
 }
 
 impl NodeBuilder {
@@ -187,12 +203,14 @@ impl NodeBuilder {
 		let chain_data_source_config = None;
 		let gossip_source_config = None;
 		let liquidity_source_config = None;
+		let payjoin_config = None;
 		Self {
 			config,
 			entropy_source_config,
 			chain_data_source_config,
 			gossip_source_config,
 			liquidity_source_config,
+			payjoin_config,
 		}
 	}
 
@@ -247,6 +265,27 @@ impl NodeBuilder {
 		self
 	}
 
+	/// Configures the [`Node`] instance to enable payjoin transactions.
+	pub fn set_payjoin_config(
+		&mut self, payjoin_directory: String, payjoin_relay: String, ohttp_keys: Option<String>,
+	) -> Result<&mut Self, BuildError> {
+		let payjoin_relay =
+			payjoin::Url::parse(&payjoin_relay).map_err(|_| BuildError::InvalidPayjoinConfig)?;
+		let payjoin_directory = payjoin::Url::parse(&payjoin_directory)
+			.map_err(|_| BuildError::InvalidPayjoinConfig)?;
+		let ohttp_keys = if let Some(ohttp_keys) = ohttp_keys {
+			let keys = match payjoin::OhttpKeys::decode(ohttp_keys.as_bytes()) {
+				Ok(keys) => keys,
+				Err(_) => return Err(BuildError::InvalidPayjoinConfig),
+			};
+			Some(keys)
+		} else {
+			None
+		};
+		self.payjoin_config = Some(PayjoinConfig { payjoin_directory, payjoin_relay, ohttp_keys });
+		Ok(self)
+	}
+
 	/// Configures the [`Node`] instance to source its inbound liquidity from the given
 	/// [LSPS2](https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md)
 	/// service.
@@ -365,6 +404,7 @@ impl NodeBuilder {
 			self.chain_data_source_config.as_ref(),
 			self.gossip_source_config.as_ref(),
 			self.liquidity_source_config.as_ref(),
+			self.payjoin_config.as_ref(),
 			seed_bytes,
 			logger,
 			vss_store,
@@ -386,6 +426,7 @@ impl NodeBuilder {
 			self.chain_data_source_config.as_ref(),
 			self.gossip_source_config.as_ref(),
 			self.liquidity_source_config.as_ref(),
+			self.payjoin_config.as_ref(),
 			seed_bytes,
 			logger,
 			kv_store,
@@ -453,6 +494,11 @@ impl ArcedNodeBuilder {
 		self.inner.write().unwrap().set_gossip_source_p2p();
 	}
 
+	/// Configures the [`Node`] instance to enable payjoin transactions.
+	pub fn set_payjoin_config(&self, payjoin_relay: String) -> Result<(), BuildError> {
+		self.inner.write().unwrap().set_payjoin_config(payjoin_relay).map(|_| ())
+	}
+
 	/// Configures the [`Node`] instance to source its gossip data from the given RapidGossipSync
 	/// server.
 	pub fn set_gossip_source_rgs(&self, rgs_server_url: String) {
@@ -521,8 +567,9 @@ impl ArcedNodeBuilder {
 fn build_with_store_internal(
 	config: Arc<Config>, chain_data_source_config: Option<&ChainDataSourceConfig>,
 	gossip_source_config: Option<&GossipSourceConfig>,
-	liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64],
-	logger: Arc<FilesystemLogger>, kv_store: Arc<DynStore>,
+	liquidity_source_config: Option<&LiquiditySourceConfig>,
+	payjoin_config: Option<&PayjoinConfig>, seed_bytes: [u8; 64], logger: Arc<FilesystemLogger>,
+	kv_store: Arc<DynStore>,
 ) -> Result<Node, BuildError> {
 	// Initialize the on-chain wallet and chain access
 	let xprv = bitcoin::bip32::ExtendedPrivKey::new_master(config.network.into(), &seed_bytes)
@@ -966,6 +1013,26 @@ fn build_with_store_internal(
 	let (stop_sender, _) = tokio::sync::watch::channel(());
 	let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(());
 
+	let mut payjoin_handler = None;
+	let mut payjoin_receiver = None;
+	if let Some(pj_config) = payjoin_config {
+		payjoin_handler = Some(Arc::new(PayjoinHandler::new(
+			pj_config.payjoin_relay.clone(),
+			Arc::clone(&tx_sync),
+			Arc::clone(&event_queue),
+			Arc::clone(&wallet),
+			Arc::clone(&payment_store),
+		)));
+		payjoin_receiver = Some(Arc::new(PayjoinReceiver::new(
+			Arc::clone(&logger),
+			Arc::clone(&wallet),
+			Arc::clone(&channel_manager),
+			Arc::clone(&config),
+			pj_config.payjoin_directory.clone(),
+			pj_config.payjoin_relay.clone(),
+			pj_config.ohttp_keys.clone(),
+		)));
+	}
 	let is_listening = Arc::new(AtomicBool::new(false));
 	let latest_wallet_sync_timestamp = Arc::new(RwLock::new(None));
 	let latest_onchain_wallet_sync_timestamp = Arc::new(RwLock::new(None));
@@ -987,6 +1054,8 @@ fn build_with_store_internal(
 		channel_manager,
 		chain_monitor,
 		output_sweeper,
+		payjoin_handler,
+		payjoin_receiver,
 		peer_manager,
 		connection_manager,
 		keys_manager,
diff --git a/src/config.rs b/src/config.rs
index d0e72080f..3d4cb6e5e 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -40,6 +40,15 @@ pub(crate) const RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL: u32 = 6;
 // The time in-between peer reconnection attempts.
 pub(crate) const PEER_RECONNECTION_INTERVAL: Duration = Duration::from_secs(10);
 
+// The time before payjoin sender requests timeout.
+pub(crate) const PAYJOIN_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
+
+// The time before payjoin sender try to send the next request.
+pub(crate) const PAYJOIN_RETRY_INTERVAL: Duration = Duration::from_secs(3);
+
+// The total time payjoin sender try to send a request.
+pub(crate) const PAYJOIN_REQUEST_TOTAL_DURATION: Duration = Duration::from_secs(24 * 60 * 60);
+
 // The time in-between RGS sync attempts.
 pub(crate) const RGS_SYNC_INTERVAL: Duration = Duration::from_secs(60 * 60);
 
diff --git a/src/error.rs b/src/error.rs
index a8671d9a7..5510a51ee 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -95,6 +95,22 @@ pub enum Error {
 	LiquiditySourceUnavailable,
 	/// The given operation failed due to the LSP's required opening fee being too high.
 	LiquidityFeeTooHigh,
+	/// Failed to access Payjoin sender object.
+	PayjoinUnavailable,
+	/// Payjoin URI is invalid.
+	PayjoinUriInvalid,
+	/// Amount is neither user-provided nor defined in the URI.
+	PayjoinRequestMissingAmount,
+	/// Failed to build a Payjoin request.
+	PayjoinRequestCreationFailed,
+	/// Payjoin response processing failed.
+	PayjoinResponseProcessingFailed,
+	/// Failed to access payjoin receiver object.
+	PayjoinReceiverUnavailable,
+	/// Failed to enroll payjoin receiver.
+	PayjoinReceiverEnrollementFailed,
+	/// Failed to validate an incoming payjoin request.
+	PayjoinReceiverRequestValidationFailed,
 }
 
 impl fmt::Display for Error {
@@ -162,6 +178,30 @@ impl fmt::Display for Error {
 			Self::LiquidityFeeTooHigh => {
 				write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
 			},
+			Self::PayjoinUnavailable => {
+				write!(f, "Failed to access Payjoin sender object. Make sure you have enabled Payjoin sending support.")
+			},
+			Self::PayjoinRequestMissingAmount => {
+				write!(f, "Amount is neither user-provided nor defined in the URI.")
+			},
+			Self::PayjoinRequestCreationFailed => {
+				write!(f, "Failed construct a Payjoin request")
+			},
+			Self::PayjoinUriInvalid => {
+				write!(f, "The provided Payjoin URI is invalid")
+			},
+			Self::PayjoinResponseProcessingFailed => {
+				write!(f, "Payjoin receiver responded to our request with an invalid response that was ignored")
+			},
+			Self::PayjoinReceiverUnavailable => {
+				write!(f, "Failed to access payjoin receiver object. Make sure you have enabled Payjoin receiving support.")
+			},
+			Self::PayjoinReceiverRequestValidationFailed => {
+				write!(f, "Failed to validate an incoming payjoin request. Payjoin sender request didnt pass the payjoin validation steps.")
+			},
+			Self::PayjoinReceiverEnrollementFailed => {
+				write!(f, "Failed to enroll payjoin receiver. Make sure the configured Payjoin directory & Payjoin relay are available.")
+			},
 		}
 	}
 }
@@ -182,3 +222,9 @@ impl From<lightning_transaction_sync::TxSyncError> for Error {
 		Self::TxSyncFailed
 	}
 }
+
+impl From<reqwest::Error> for Error {
+	fn from(_e: reqwest::Error) -> Self {
+		Self::PayjoinRequestCreationFailed
+	}
+}
diff --git a/src/event.rs b/src/event.rs
index 838df4230..bb04d55f8 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -1,3 +1,4 @@
+use crate::payjoin_receiver::PayjoinReceiver;
 use crate::types::{DynStore, Sweeper, Wallet};
 
 use crate::{
@@ -24,6 +25,7 @@ use lightning::events::{ClosureReason, PaymentPurpose};
 use lightning::events::{Event as LdkEvent, PaymentFailureReason};
 use lightning::impl_writeable_tlv_based_enum;
 use lightning::ln::channelmanager::PaymentId;
+use lightning::ln::msgs::DecodeError;
 use lightning::ln::{ChannelId, PaymentHash};
 use lightning::routing::gossip::NodeId;
 use lightning::util::errors::APIError;
@@ -143,6 +145,73 @@ pub enum Event {
 		/// This will be `None` for events serialized by LDK Node v0.2.1 and prior.
 		reason: Option<ClosureReason>,
 	},
+	/// Failed to send Payjoin transaction.
+	///
+	/// This event is emitted when our attempt to send Payjoin transaction fail.
+	PayjoinPaymentPending {
+		/// Transaction ID of the successfully sent Payjoin transaction.
+		txid: bitcoin::Txid,
+		/// docs
+		amount: u64,
+		/// docs
+		receipient: bitcoin::ScriptBuf,
+	},
+	/// A Payjoin transaction has been successfully sent.
+	///
+	/// This event is emitted when we send a Payjoin transaction and it was accepted by the
+	/// receiver, and then finalised and broadcasted by us.
+	PayjoinPaymentSuccess {
+		/// Transaction ID of the successfully sent Payjoin transaction.
+		txid: bitcoin::Txid,
+		/// docs
+		amount: u64,
+		/// docs
+		receipient: bitcoin::ScriptBuf,
+	},
+	/// Failed to send Payjoin transaction.
+	///
+	/// This event is emitted when our attempt to send Payjoin transaction fail.
+	PayjoinPaymentFailed {
+		/// Transaction ID of the successfully sent Payjoin transaction.
+		txid: Option<bitcoin::Txid>,
+		/// docs
+		amount: u64,
+		/// docs
+		receipient: bitcoin::ScriptBuf,
+		/// Reason for the failure.
+		reason: PayjoinPaymentFailureReason,
+	},
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum PayjoinPaymentFailureReason {
+	Timeout,
+	TransactionFinalisationFailed,
+	InvalidReceiverResponse,
+	RequestFailed,
+}
+
+impl Readable for PayjoinPaymentFailureReason {
+	fn read<R: std::io::Read>(reader: &mut R) -> Result<Self, DecodeError> {
+		match u8::read(reader)? {
+			0 => Ok(Self::Timeout),
+			1 => Ok(Self::TransactionFinalisationFailed),
+			2 => Ok(Self::InvalidReceiverResponse),
+			3 => Ok(Self::RequestFailed),
+			_ => Err(DecodeError::InvalidValue),
+		}
+	}
+}
+
+impl Writeable for PayjoinPaymentFailureReason {
+	fn write<W: Writer>(&self, writer: &mut W) -> Result<(), std::io::Error> {
+		match *self {
+			Self::Timeout => 0u8.write(writer),
+			Self::TransactionFinalisationFailed => 1u8.write(writer),
+			Self::InvalidReceiverResponse => 2u8.write(writer),
+			Self::RequestFailed => 3u8.write(writer),
+		}
+	}
 }
 
 impl_writeable_tlv_based_enum!(Event,
@@ -184,6 +253,22 @@ impl_writeable_tlv_based_enum!(Event,
 		(2, payment_id, required),
 		(4, claimable_amount_msat, required),
 		(6, claim_deadline, option),
+	},
+	(7, PayjoinPaymentPending) => {
+		(0, txid, required),
+		(2, amount, required),
+		(4, receipient, required),
+	},
+	(8, PayjoinPaymentSuccess) => {
+		(0, txid, required),
+		(2, amount, required),
+		(4, receipient, required),
+	},
+	(9, PayjoinPaymentFailed) => {
+		(0, amount, required),
+		(1, txid, option),
+		(2, receipient, required),
+		(4, reason, required),
 	};
 );
 
@@ -354,6 +439,7 @@ where
 	network_graph: Arc<Graph>,
 	payment_store: Arc<PaymentStore<L>>,
 	peer_store: Arc<PeerStore<L>>,
+	payjoin_receiver: Option<Arc<PayjoinReceiver>>,
 	runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>,
 	logger: L,
 	config: Arc<Config>,
@@ -368,8 +454,9 @@ where
 		bump_tx_event_handler: Arc<BumpTransactionEventHandler>,
 		channel_manager: Arc<ChannelManager>, connection_manager: Arc<ConnectionManager<L>>,
 		output_sweeper: Arc<Sweeper>, network_graph: Arc<Graph>,
-		payment_store: Arc<PaymentStore<L>>, peer_store: Arc<PeerStore<L>>,
-		runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>, logger: L, config: Arc<Config>,
+		payment_store: Arc<PaymentStore<L>>, payjoin_receiver: Option<Arc<PayjoinReceiver>>,
+		peer_store: Arc<PeerStore<L>>, runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>,
+		logger: L, config: Arc<Config>,
 	) -> Self {
 		Self {
 			event_queue,
@@ -380,6 +467,7 @@ where
 			output_sweeper,
 			network_graph,
 			payment_store,
+			payjoin_receiver,
 			peer_store,
 			logger,
 			runtime,
@@ -394,6 +482,7 @@ where
 				counterparty_node_id,
 				channel_value_satoshis,
 				output_script,
+				user_channel_id,
 				..
 			} => {
 				// Construct the raw transaction with the output that is paid the amount of the
@@ -404,6 +493,18 @@ where
 				let cur_height = self.channel_manager.current_best_block().height;
 				let locktime = LockTime::from_height(cur_height).unwrap_or(LockTime::ZERO);
 
+				if let Some(payjoin_receiver) = self.payjoin_receiver.clone() {
+					if payjoin_receiver
+						.set_channel_accepted(
+							user_channel_id,
+							&output_script,
+							temporary_channel_id.0,
+						)
+						.await
+					{
+						return;
+					}
+				}
 				// Sign the final funding transaction and broadcast it.
 				match self.wallet.create_funding_transaction(
 					output_script,
@@ -1066,6 +1167,45 @@ where
 					);
 				}
 			},
+			LdkEvent::FundingTxBroadcastSafe { funding_tx, .. } => {
+				use crate::io::utils::ohttp_headers;
+				if let Some(payjoin_receiver) = self.payjoin_receiver.clone() {
+					let is_payjoin_channel =
+						payjoin_receiver.set_funding_tx_signed(funding_tx.clone()).await;
+					if let Some((url, body)) = is_payjoin_channel {
+						log_info!(
+							self.logger,
+							"Detected payjoin channel transaction. Sending payjoin sender request for transaction {}",
+							funding_tx.txid()
+						);
+						let headers = ohttp_headers();
+						let client = reqwest::Client::builder().build().unwrap();
+						match client.post(url).body(body).headers(headers).send().await {
+							Ok(response) => {
+								if response.status().is_success() {
+									log_info!(
+										self.logger,
+										"Responded to 'Payjoin Sender' successfuly"
+									);
+								} else {
+									log_info!(
+										self.logger,
+										"Got unsuccessful response from 'Payjoin Sender': {}",
+										response.status()
+									);
+								}
+							},
+							Err(e) => {
+								log_error!(
+									self.logger,
+									"Failed to send a response to 'Payjoin Sender': {}",
+									e
+								);
+							},
+						};
+					}
+				}
+			},
 			LdkEvent::ChannelPending {
 				channel_id,
 				user_channel_id,
diff --git a/src/io/utils.rs b/src/io/utils.rs
index 77cc56f55..d298318f5 100644
--- a/src/io/utils.rs
+++ b/src/io/utils.rs
@@ -511,6 +511,15 @@ pub(crate) fn check_namespace_key_validity(
 	Ok(())
 }
 
+pub(crate) fn ohttp_headers() -> reqwest::header::HeaderMap {
+	let mut headers = reqwest::header::HeaderMap::new();
+	headers.insert(
+		reqwest::header::CONTENT_TYPE,
+		reqwest::header::HeaderValue::from_static("message/ohttp-req"),
+	);
+	headers
+}
+
 #[cfg(test)]
 mod tests {
 	use super::*;
diff --git a/src/lib.rs b/src/lib.rs
index de2a0badf..71d2f0953 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -89,6 +89,8 @@ pub mod io;
 mod liquidity;
 mod logger;
 mod message_handler;
+mod payjoin_channel_scheduler;
+mod payjoin_receiver;
 pub mod payment;
 mod peer_store;
 mod sweep;
@@ -109,6 +111,8 @@ pub use error::Error as NodeError;
 use error::Error;
 
 pub use event::Event;
+use payjoin_receiver::PayjoinReceiver;
+use payment::payjoin::handler::PayjoinHandler;
 pub use types::ChannelConfig;
 
 pub use io::utils::generate_entropy_mnemonic;
@@ -133,7 +137,10 @@ use gossip::GossipSource;
 use graph::NetworkGraph;
 use liquidity::LiquiditySource;
 use payment::store::PaymentStore;
-use payment::{Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment};
+use payment::{
+	Bolt11Payment, Bolt12Payment, OnchainPayment, PayjoinPayment, PaymentDetails,
+	SpontaneousPayment,
+};
 use peer_store::{PeerInfo, PeerStore};
 use types::{
 	Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, FeeEstimator,
@@ -185,6 +192,8 @@ pub struct Node {
 	output_sweeper: Arc<Sweeper>,
 	peer_manager: Arc<PeerManager>,
 	connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
+	payjoin_handler: Option<Arc<PayjoinHandler>>,
+	payjoin_receiver: Option<Arc<PayjoinReceiver>>,
 	keys_manager: Arc<KeysManager>,
 	network_graph: Arc<Graph>,
 	gossip_source: Arc<GossipSource>,
@@ -365,6 +374,10 @@ impl Node {
 		let archive_cmon = Arc::clone(&self.chain_monitor);
 		let sync_sweeper = Arc::clone(&self.output_sweeper);
 		let sync_logger = Arc::clone(&self.logger);
+		let sync_payjoin = match &self.payjoin_handler {
+			Some(pjh) => Some(Arc::clone(pjh)),
+			None => None,
+		};
 		let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp);
 		let sync_monitor_archival_height = Arc::clone(&self.latest_channel_monitor_archival_height);
 		let mut stop_sync = self.stop_sender.subscribe();
@@ -384,11 +397,14 @@ impl Node {
 						return;
 					}
 					_ = wallet_sync_interval.tick() => {
-						let confirmables = vec![
+						let mut confirmables = vec![
 							&*sync_cman as &(dyn Confirm + Sync + Send),
 							&*sync_cmon as &(dyn Confirm + Sync + Send),
 							&*sync_sweeper as &(dyn Confirm + Sync + Send),
 						];
+						if let Some(sync_payjoin) = sync_payjoin.as_ref() {
+							confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send));
+						}
 						let now = Instant::now();
 						let timeout_fut = tokio::time::timeout(Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), tx_sync.sync(confirmables));
 						match timeout_fut.await {
@@ -685,6 +701,30 @@ impl Node {
 			Arc::clone(&self.logger),
 		));
 
+		// Check every 5 seconds if we have received a payjoin transaction to our enrolled
+		// subdirectory with the configured Payjoin directory.
+		if let Some(payjoin_receiver) = &self.payjoin_receiver {
+			let mut stop_payjoin_server = self.stop_sender.subscribe();
+			let payjoin_receiver = Arc::clone(&payjoin_receiver);
+			let payjoin_check_interval = 5;
+			runtime.spawn(async move {
+				let mut payjoin_interval =
+					tokio::time::interval(Duration::from_secs(payjoin_check_interval));
+				payjoin_interval.reset();
+				payjoin_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
+				loop {
+					tokio::select! {
+						_ = stop_payjoin_server.changed() => {
+							return;
+						}
+						_ = payjoin_interval.tick() => {
+							let _ = payjoin_receiver.process_payjoin_request().await;
+						}
+					}
+				}
+			});
+		}
+
 		let event_handler = Arc::new(EventHandler::new(
 			Arc::clone(&self.event_queue),
 			Arc::clone(&self.wallet),
@@ -694,6 +734,7 @@ impl Node {
 			Arc::clone(&self.output_sweeper),
 			Arc::clone(&self.network_graph),
 			Arc::clone(&self.payment_store),
+			self.payjoin_receiver.clone(),
 			Arc::clone(&self.peer_store),
 			Arc::clone(&self.runtime),
 			Arc::clone(&self.logger),
@@ -1063,6 +1104,56 @@ impl Node {
 		))
 	}
 
+	/// Returns a payment handler allowing to send payjoin payments.
+	///
+	/// In order to utilize the Payjoin functionality, it's necessary
+	/// to configure your node using [`set_payjoin_config`].
+	///
+	/// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config
+	#[cfg(not(feature = "uniffi"))]
+	pub fn payjoin_payment(&self) -> PayjoinPayment {
+		let payjoin_handler = self.payjoin_handler.as_ref();
+		let payjoin_receiver = self.payjoin_receiver.as_ref();
+		PayjoinPayment::new(
+			Arc::clone(&self.runtime),
+			payjoin_handler.map(Arc::clone),
+			payjoin_receiver.map(Arc::clone),
+			Arc::clone(&self.config),
+			Arc::clone(&self.logger),
+			Arc::clone(&self.wallet),
+			Arc::clone(&self.tx_broadcaster),
+			Arc::clone(&self.peer_store),
+			Arc::clone(&self.channel_manager),
+			Arc::clone(&self.connection_manager),
+			Arc::clone(&self.payment_store),
+		)
+	}
+
+	/// Returns a payment handler allowing to send payjoin payments.
+	///
+	/// In order to utilize the Payjoin functionality, it's necessary
+	/// to configure your node using [`set_payjoin_config`].
+	///
+	/// [`set_payjoin_config`]: crate::builder::NodeBuilder::set_payjoin_config
+	#[cfg(feature = "uniffi")]
+	pub fn payjoin_payment(&self) -> Arc<PayjoinPayment> {
+		let payjoin_handler = self.payjoin_handler.as_ref();
+		let payjoin_receiver = self.payjoin_receiver.as_ref();
+		Arc::new(PayjoinPayment::new(
+			Arc::clone(&self.runtime),
+			payjoin_handler.map(Arc::clone),
+			payjoin_receiver.map(Arc::clone),
+			Arc::clone(&self.config),
+			Arc::clone(&self.logger),
+			Arc::clone(&self.wallet),
+			Arc::clone(&self.tx_broadcaster),
+			Arc::clone(&self.peer_store),
+			Arc::clone(&self.channel_manager),
+			Arc::clone(&self.connection_manager),
+			Arc::clone(&self.payment_store),
+		))
+	}
+
 	/// Retrieve a list of known channels.
 	pub fn list_channels(&self) -> Vec<ChannelDetails> {
 		self.channel_manager.list_channels().into_iter().map(|c| c.into()).collect()
@@ -1264,11 +1355,15 @@ impl Node {
 		let fee_estimator = Arc::clone(&self.fee_estimator);
 		let sync_sweeper = Arc::clone(&self.output_sweeper);
 		let sync_logger = Arc::clone(&self.logger);
-		let confirmables = vec![
+		let sync_payjoin = &self.payjoin_handler.as_ref();
+		let mut confirmables = vec![
 			&*sync_cman as &(dyn Confirm + Sync + Send),
 			&*sync_cmon as &(dyn Confirm + Sync + Send),
 			&*sync_sweeper as &(dyn Confirm + Sync + Send),
 		];
+		if let Some(sync_payjoin) = sync_payjoin {
+			confirmables.push(sync_payjoin.as_ref() as &(dyn Confirm + Sync + Send));
+		}
 		let sync_wallet_timestamp = Arc::clone(&self.latest_wallet_sync_timestamp);
 		let sync_fee_rate_update_timestamp =
 			Arc::clone(&self.latest_fee_rate_cache_update_timestamp);
diff --git a/src/payjoin_channel_scheduler.rs b/src/payjoin_channel_scheduler.rs
new file mode 100644
index 000000000..6f30e3252
--- /dev/null
+++ b/src/payjoin_channel_scheduler.rs
@@ -0,0 +1,251 @@
+use bitcoin::{secp256k1::PublicKey, Network, ScriptBuf, TxOut};
+
+#[derive(Clone)]
+pub struct PayjoinChannelScheduler {
+	channels: Vec<PayjoinChannel>,
+}
+
+impl PayjoinChannelScheduler {
+	pub(crate) fn new() -> Self {
+		Self { channels: vec![] }
+	}
+
+	pub(crate) fn schedule(
+		&mut self, channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey,
+		channel_id: u128,
+	) {
+		let channel = PayjoinChannel::new(channel_value_satoshi, counterparty_node_id, channel_id);
+		match channel.state {
+			ScheduledChannelState::ChannelCreated => {
+				self.channels.push(channel);
+			},
+			_ => {},
+		}
+	}
+
+	pub(crate) fn set_channel_accepted(
+		&mut self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32],
+	) -> bool {
+		for channel in &mut self.channels {
+			if channel.channel_id() == channel_id {
+				channel.state.set_channel_accepted(output_script, temporary_channel_id);
+				return true;
+			}
+		}
+		false
+	}
+
+	pub(crate) fn set_funding_tx_created(
+		&mut self, channel_id: u128, url: &payjoin::Url, body: Vec<u8>,
+	) -> bool {
+		for channel in &mut self.channels {
+			if channel.channel_id() == channel_id {
+				return channel.state.set_channel_funding_tx_created(url.clone(), body);
+			}
+		}
+		false
+	}
+
+	pub(crate) fn set_funding_tx_signed(
+		&mut self, tx: bitcoin::Transaction,
+	) -> Option<(payjoin::Url, Vec<u8>)> {
+		for output in tx.output.iter() {
+			if let Some(mut channel) = self.internal_find_by_tx_out(&output.clone()) {
+				let info = channel.request_info();
+				if info.is_some() && channel.state.set_channel_funding_tx_signed(output.clone()) {
+					return info;
+				}
+			}
+		}
+		None
+	}
+
+	/// Get the next channel matching the given channel amount.
+	///
+	/// The channel must be in accepted state.
+	///
+	/// If more than one channel matches the given channel amount, the channel with the oldest
+	/// creation date will be returned.
+	pub(crate) fn get_next_channel(
+		&self, channel_amount: bitcoin::Amount, network: Network,
+	) -> Option<(u128, bitcoin::Address, [u8; 32], bitcoin::Amount, bitcoin::secp256k1::PublicKey)>
+	{
+		let channel = self
+			.channels
+			.iter()
+			.filter(|channel| {
+				channel.channel_value_satoshi() == channel_amount
+					&& channel.is_channel_accepted()
+					&& channel.output_script().is_some()
+					&& channel.temporary_channel_id().is_some()
+			})
+			.min_by_key(|channel| channel.created_at());
+
+		if let Some(channel) = channel {
+			let address = bitcoin::Address::from_script(&channel.output_script().unwrap(), network);
+			if let Ok(address) = address {
+				return Some((
+					channel.channel_id(),
+					address,
+					channel.temporary_channel_id().unwrap(),
+					channel.channel_value_satoshi(),
+					channel.counterparty_node_id(),
+				));
+			}
+		};
+		None
+	}
+
+	fn internal_find_by_tx_out(&self, txout: &TxOut) -> Option<PayjoinChannel> {
+		let channel = self.channels.iter().find(|channel| {
+			return Some(&txout.script_pubkey) == channel.output_script();
+		});
+		channel.cloned()
+	}
+}
+
+#[derive(Clone, Debug)]
+pub(crate) struct PayjoinChannel {
+	state: ScheduledChannelState,
+	channel_value_satoshi: bitcoin::Amount,
+	channel_id: u128,
+	counterparty_node_id: PublicKey,
+	created_at: u64,
+}
+
+impl PayjoinChannel {
+	pub(crate) fn new(
+		channel_value_satoshi: bitcoin::Amount, counterparty_node_id: PublicKey, channel_id: u128,
+	) -> Self {
+		Self {
+			state: ScheduledChannelState::ChannelCreated,
+			channel_value_satoshi,
+			channel_id,
+			counterparty_node_id,
+			created_at: 0,
+		}
+	}
+
+	fn is_channel_accepted(&self) -> bool {
+		match self.state {
+			ScheduledChannelState::ChannelAccepted(..) => true,
+			_ => false,
+		}
+	}
+
+	pub(crate) fn channel_value_satoshi(&self) -> bitcoin::Amount {
+		self.channel_value_satoshi
+	}
+
+	pub(crate) fn channel_id(&self) -> u128 {
+		self.channel_id
+	}
+
+	pub(crate) fn counterparty_node_id(&self) -> PublicKey {
+		self.counterparty_node_id
+	}
+
+	pub(crate) fn output_script(&self) -> Option<&ScriptBuf> {
+		self.state.output_script()
+	}
+
+	pub(crate) fn temporary_channel_id(&self) -> Option<[u8; 32]> {
+		self.state.temporary_channel_id()
+	}
+
+	pub(crate) fn request_info(&self) -> Option<(payjoin::Url, Vec<u8>)> {
+		match &self.state {
+			ScheduledChannelState::FundingTxCreated(_, url, body) => {
+				Some((url.clone(), body.clone()))
+			},
+			_ => None,
+		}
+	}
+
+	fn created_at(&self) -> u64 {
+		self.created_at
+	}
+}
+
+#[derive(Clone, Debug)]
+struct FundingTxParams {
+	output_script: ScriptBuf,
+	temporary_channel_id: [u8; 32],
+}
+
+impl FundingTxParams {
+	fn new(output_script: ScriptBuf, temporary_channel_id: [u8; 32]) -> Self {
+		Self { output_script, temporary_channel_id }
+	}
+}
+
+#[derive(Clone, Debug)]
+enum ScheduledChannelState {
+	ChannelCreated,
+	ChannelAccepted(FundingTxParams),
+	FundingTxCreated(FundingTxParams, payjoin::Url, Vec<u8>),
+	FundingTxSigned(FundingTxParams, ()),
+}
+
+impl ScheduledChannelState {
+	fn output_script(&self) -> Option<&ScriptBuf> {
+		match self {
+			ScheduledChannelState::ChannelAccepted(funding_tx_params) => {
+				Some(&funding_tx_params.output_script)
+			},
+			ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) => {
+				Some(&funding_tx_params.output_script)
+			},
+			ScheduledChannelState::FundingTxSigned(funding_tx_params, _) => {
+				Some(&funding_tx_params.output_script)
+			},
+			_ => None,
+		}
+	}
+
+	fn temporary_channel_id(&self) -> Option<[u8; 32]> {
+		match self {
+			ScheduledChannelState::ChannelAccepted(funding_tx_params) => {
+				Some(funding_tx_params.temporary_channel_id)
+			},
+			ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) => {
+				Some(funding_tx_params.temporary_channel_id)
+			},
+			ScheduledChannelState::FundingTxSigned(funding_tx_params, _) => {
+				Some(funding_tx_params.temporary_channel_id)
+			},
+			_ => None,
+		}
+	}
+
+	fn set_channel_accepted(
+		&mut self, output_script: &ScriptBuf, temporary_channel_id: [u8; 32],
+	) -> bool {
+		if let ScheduledChannelState::ChannelCreated = self {
+			*self = ScheduledChannelState::ChannelAccepted(FundingTxParams::new(
+				output_script.clone(),
+				temporary_channel_id,
+			));
+			return true;
+		}
+		return false;
+	}
+
+	fn set_channel_funding_tx_created(&mut self, url: payjoin::Url, body: Vec<u8>) -> bool {
+		if let ScheduledChannelState::ChannelAccepted(funding_tx_params) = self {
+			*self = ScheduledChannelState::FundingTxCreated(funding_tx_params.clone(), url, body);
+			return true;
+		}
+		return false;
+	}
+
+	fn set_channel_funding_tx_signed(&mut self, output: TxOut) -> bool {
+		let mut res = false;
+		if let ScheduledChannelState::FundingTxCreated(funding_tx_params, _, _) = self {
+			assert_eq!(funding_tx_params.output_script, output.script_pubkey);
+			*self = ScheduledChannelState::FundingTxSigned(funding_tx_params.clone(), ());
+			res = true;
+		}
+		return res;
+	}
+}
diff --git a/src/payjoin_receiver.rs b/src/payjoin_receiver.rs
new file mode 100644
index 000000000..c1ef51061
--- /dev/null
+++ b/src/payjoin_receiver.rs
@@ -0,0 +1,540 @@
+use crate::error::Error;
+use crate::io::utils::ohttp_headers;
+use crate::logger::FilesystemLogger;
+use crate::payjoin_channel_scheduler::{PayjoinChannel, PayjoinChannelScheduler};
+use crate::types::{ChannelManager, Wallet};
+use crate::Config;
+use bitcoin::{ScriptBuf, Transaction};
+use lightning::ln::ChannelId;
+use lightning::log_info;
+use lightning::util::logger::Logger;
+use payjoin::receive::v2::{Enrolled, Enroller, ProvisionalProposal, UncheckedProposal};
+use payjoin::{OhttpKeys, PjUriBuilder};
+use payjoin::{PjUri, Url};
+use std::ops::Deref;
+use std::sync::Arc;
+use tokio::sync::RwLock;
+
+/// Implements Payjoin protocol as specified in [BIP77]
+///
+/// [BIP77]: https://github.com/bitcoin/bips/blob/3b863a402e0250658985f08a455a6cd103e269e5/bip-0077.mediawiki
+pub(crate) struct PayjoinReceiver {
+	logger: Arc<FilesystemLogger>,
+	wallet: Arc<Wallet>,
+	channel_manager: Arc<ChannelManager>,
+	channel_scheduler: RwLock<PayjoinChannelScheduler>,
+	/// Directory receiver wish to enroll with
+	payjoin_directory: Url,
+	/// Proxy server receiver wish to make requests through
+	payjoin_relay: Url,
+	/// Enrollement object indicates a successful enrollement if is defined.
+	enrolled: RwLock<Option<Enrolled>>,
+	/// Directory keys.
+	///
+	/// Optional as they can be fetched on behalf of the user if not provided.
+	/// They are required in order to enroll.
+	ohttp_keys: RwLock<Option<OhttpKeys>>,
+	config: Arc<Config>,
+}
+
+impl PayjoinReceiver {
+	pub(crate) fn new(
+		logger: Arc<FilesystemLogger>, wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>,
+		config: Arc<Config>, payjoin_directory: payjoin::Url, payjoin_relay: payjoin::Url,
+		ohttp_keys: Option<OhttpKeys>,
+	) -> Self {
+		Self {
+			logger,
+			wallet,
+			channel_manager,
+			channel_scheduler: RwLock::new(PayjoinChannelScheduler::new()),
+			config,
+			payjoin_directory,
+			payjoin_relay,
+			enrolled: RwLock::new(None),
+			ohttp_keys: RwLock::new(ohttp_keys),
+		}
+	}
+
+	/// Before receiving Payjoin transactions we `enroll` with a Payjoin directory
+	/// and we acquire a subdirectory we can receive Payjoin transactions to while offline.
+	///
+	/// This function returns [BIP21] URI with Payjoin parameters.
+	///
+	/// [BIP21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
+	pub(crate) async fn receive(&self, amount: bitcoin::Amount) -> Result<PjUri, Error> {
+		if !self.is_enrolled().await {
+			self.enroll().await?;
+		}
+		let enrolled = self.enrolled.read().await;
+		let enrolled = match enrolled.as_ref() {
+			Some(enrolled) => enrolled,
+			None => {
+				log_info!(self.logger, "Payjoin Receiver: Not enrolled");
+				return Err(Error::PayjoinReceiverUnavailable);
+			},
+		};
+		let fallback_target = enrolled.fallback_target();
+		let ohttp_keys = self.ohttp_keys.read().await;
+		let ohttp_keys = match ohttp_keys.as_ref() {
+			Some(okeys) => okeys,
+			None => {
+				log_info!(self.logger, "Payjoin Receiver: No ohttp keys");
+				return Err(Error::PayjoinReceiverUnavailable);
+			},
+		};
+		let address = self.wallet.get_new_address()?;
+		let pj_part = match payjoin::Url::parse(&fallback_target) {
+			Ok(pj_part) => pj_part,
+			Err(_) => {
+				log_info!(self.logger, "Payjoin Receiver: Invalid fallback target");
+				return Err(Error::PayjoinReceiverUnavailable);
+			},
+		};
+		let payjoin_uri =
+			PjUriBuilder::new(address, pj_part, Some(ohttp_keys.clone())).amount(amount).build();
+		Ok(payjoin_uri)
+	}
+
+	pub(crate) async fn set_channel_accepted(
+		&self, channel_id: u128, output_script: &ScriptBuf, temporary_channel_id: [u8; 32],
+	) -> bool {
+		let mut scheduler = self.channel_scheduler.write().await;
+		scheduler.set_channel_accepted(channel_id, output_script, temporary_channel_id)
+	}
+
+	/// After enrolling, we should periodacly check if we have received any Payjoin transactions.
+	///
+	/// This function will try to fetch pending Payjoin requests from the subdirectory, and if a
+	/// successful response received, we validate the request as specified in [BIP78]. After
+	/// validation we check if we have a pending matching channel, and if so, we try fund the channel
+	/// with the incoming funds from the payjoin request. Otherwise, we accept the Payjoin request
+	/// normally by trying to preserve privacy, finalise the Payjoin proposal and send it back the
+	/// the Payjoin sender.
+	///
+	/// [BIP78]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_original_PSBT_checklist
+	pub(crate) async fn process_payjoin_request(&self) {
+		let mut enrolled = self.enrolled.write().await;
+		if let Some(mut enrolled) = enrolled.take() {
+			let (req, context) = match enrolled.extract_req() {
+				Ok(req) => req,
+				Err(e) => {
+					log_info!(
+						self.logger,
+						"Payjoin Receiver: Unable to extract enrollement request and context{}",
+						e
+					);
+					return;
+				},
+			};
+
+			let client = reqwest::Client::new();
+			let response = match client
+				.post(req.url.to_string())
+				.body(req.body)
+				.headers(ohttp_headers())
+				.send()
+				.await
+			{
+				Ok(response) => response,
+				Err(e) => {
+					log_info!(
+						self.logger,
+						"Payjoin Receiver: Unable to fetch payjoin request {}",
+						e
+					);
+					return;
+				},
+			};
+			if response.status() != reqwest::StatusCode::OK {
+				log_info!(
+					self.logger,
+					"Payjoin Receiver: Got non-200 response from directory server {}",
+					response.status()
+				);
+				return;
+			};
+			let response = match response.bytes().await {
+				Ok(response) => response,
+				Err(e) => {
+					log_info!(self.logger, "Payjoin Receiver: Error reading response {}", e);
+					return;
+				},
+			};
+			if response.is_empty() {
+				log_info!(self.logger, "Payjoin Receiver: Empty response from directory server");
+				return;
+			};
+			let response = match enrolled.process_res(response.to_vec().as_slice(), context) {
+				Ok(response) => response,
+				Err(e) => {
+					log_info!(
+						self.logger,
+						"Payjoin Receiver: Unable to process payjoin request {}",
+						e
+					);
+					return;
+				},
+			};
+			let unchecked_proposal = match response {
+				Some(proposal) => proposal,
+				None => {
+					return;
+				},
+			};
+			let original_tx = unchecked_proposal.extract_tx_to_schedule_broadcast();
+			let provisional_proposal = match self.validate_payjoin_request(unchecked_proposal).await
+			{
+				Ok(proposal) => proposal,
+				Err(e) => {
+					log_info!(self.logger, "Payjoin Validation: {}", e);
+					return;
+				},
+			};
+			let amount = match self.wallet.funds_directed_to_us(&original_tx) {
+				Ok(a) => a,
+				Err(e) => {
+					// This should not happen in practice as the validation checks would fail if
+					// the sender didnt include us in the outputs
+					log_info!(self.logger, "Not able to find any ouput directed to us: {}", e);
+					return;
+				},
+			};
+			let mut scheduler = self.channel_scheduler.write().await;
+			let network = self.config.network;
+			if let Some(channel) = scheduler.get_next_channel(amount, network) {
+				log_info!(self.logger, "Found a channel match for incoming Payjoin request");
+				let (channel_id, funding_tx_address, temporary_channel_id, _, counterparty_node_id) =
+					channel;
+				let mut channel_provisional_proposal = provisional_proposal.clone();
+				channel_provisional_proposal.substitute_output_address(funding_tx_address);
+				let payjoin_proposal = match channel_provisional_proposal
+					.finalize_proposal(|psbt| Ok(psbt.clone()), None)
+				{
+					Ok(proposal) => proposal,
+					Err(e) => {
+						dbg!(&e);
+						return;
+					},
+				};
+				let (receiver_request, _) = match payjoin_proposal.clone().extract_v2_req() {
+					Ok((req, ctx)) => (req, ctx),
+					Err(e) => {
+						dbg!(&e);
+						return;
+					},
+				};
+				let tx = payjoin_proposal.psbt().clone().extract_tx();
+				scheduler.set_funding_tx_created(
+					channel_id,
+					&receiver_request.url,
+					receiver_request.body,
+				);
+				match self.channel_manager.unsafe_manual_funding_transaction_generated(
+					&ChannelId::from_bytes(temporary_channel_id),
+					&counterparty_node_id,
+					tx.clone(),
+				) {
+					Ok(_) => {
+						// Created Funding Transaction and waiting for `FundingTxBroadcastSafe` event before returning a response
+						log_info!(self.logger, "Created channel funding transaction from Payjoin request and waiting for `FundingTxBroadcastSafe`");
+					},
+					Err(_) => {
+						log_info!(
+							self.logger,
+							"Unable to channel create funding tx from Payjoin request"
+						);
+					},
+				}
+			} else {
+				log_info!(
+					self.logger,
+					"Couldnt match a channel to Payjoin request, accepting normally"
+				);
+				self.accept_payjoin_transaction(provisional_proposal).await;
+			}
+		} else {
+			log_info!(self.logger, "Payjoin Receiver: Unable to get enrolled object");
+		}
+	}
+
+	async fn accept_payjoin_transaction(&self, mut provisional_proposal: ProvisionalProposal) {
+		// Preserve privacy
+		let (candidate_inputs, utxo_set) = match self.wallet.payjoin_receiver_candidate_input() {
+			Ok(a) => a,
+			Err(e) => {
+				log_info!(self.logger, "Didnt find candidate inputs: {}", e);
+				return;
+			},
+		};
+		match provisional_proposal.try_preserving_privacy(candidate_inputs) {
+			Ok(selected_outpoint) => {
+				if let Some(selected_utxo) = utxo_set.iter().find(|i| {
+					i.outpoint.txid == selected_outpoint.txid
+						&& i.outpoint.vout == selected_outpoint.vout
+				}) {
+					let txo_to_contribute = bitcoin::TxOut {
+						value: selected_utxo.txout.value,
+						script_pubkey: selected_utxo.txout.script_pubkey.clone(),
+					};
+					let outpoint_to_contribute = bitcoin::OutPoint {
+						txid: selected_utxo.outpoint.txid,
+						vout: selected_utxo.outpoint.vout,
+					};
+					provisional_proposal
+						.contribute_witness_input(txo_to_contribute, outpoint_to_contribute);
+				}
+			},
+			Err(_) => {
+				log_info!(self.logger, "Failed to select utxos to improve payjoin request privacy. Payjoin proceeds regardless");
+			},
+		};
+		// Finalise Payjoin Proposal
+		let mut payjoin_proposal = match provisional_proposal.finalize_proposal(
+			|psbt| {
+				self.wallet.prepare_payjoin_proposal(psbt.clone()).map_err(|e| {
+					log_info!(self.logger, "Payjoin Receiver: Unable to sign proposal {}", e);
+					payjoin::Error::Server(e.into())
+				})
+			},
+			None,
+		) {
+			Ok(proposal) => proposal,
+			Err(e) => {
+				log_info!(self.logger, "Payjoin Receiver: Unable to finalize proposal {}", e);
+				return;
+			},
+		};
+
+		let (receiver_request, _) = match payjoin_proposal.extract_v2_req() {
+			Ok(req) => req,
+			Err(e) => {
+				log_info!(self.logger, "Payjoin Receiver: Unable to extract V2 request {}", e);
+				return;
+			},
+		};
+		// Send Payjoin Proposal response back to Payjoin sender
+		match reqwest::Client::new()
+			.post(&receiver_request.url.to_string())
+			.body(receiver_request.body)
+			.headers(ohttp_headers())
+			.send()
+			.await
+		{
+			Ok(response) => {
+				if response.status() == reqwest::StatusCode::OK {
+					log_info!(self.logger, "Payjoin Receiver: Payjoin response sent to sender");
+				} else {
+					log_info!(
+						self.logger,
+						"Payjoin Receiver: Got non-200 response from directory {}",
+						response.status()
+					);
+				}
+			},
+			Err(e) => {
+				log_info!(
+					self.logger,
+					"Payjoin Receiver: Unable to make request to directory {}",
+					e
+				);
+			},
+		};
+	}
+
+	/// Enrolls a Payjoin receiver with the specified Payjoin directory.
+	///
+	/// If directory `ohttp_keys` are not provided, they will be fetched from the directory using
+	/// the Payjoin relay as proxy to improve privacy and not expose users IP address.
+	async fn enroll(&self) -> Result<(), Error> {
+		let ohttp_keys = match self.ohttp_keys.read().await.deref() {
+			Some(okeys) => okeys.clone(),
+			None => {
+				let payjoin_directory = &self.payjoin_directory;
+				let payjoin_directory = match payjoin_directory.join("/ohttp-keys") {
+					Ok(payjoin_directory) => payjoin_directory,
+					Err(e) => {
+						log_info!(
+							self.logger,
+							"Payjoin Receiver: Unable to construct ohttp keys url {}",
+							e
+						);
+						return Err(Error::PayjoinReceiverEnrollementFailed);
+					},
+				};
+				let proxy = match reqwest::Proxy::all(self.payjoin_relay.to_string()) {
+					Ok(proxy) => proxy,
+					Err(e) => {
+						log_info!(
+							self.logger,
+							"Payjoin Receiver: Unable to construct reqwest proxy {}",
+							e
+						);
+						return Err(Error::PayjoinReceiverEnrollementFailed);
+					},
+				};
+				let client = match reqwest::Client::builder().proxy(proxy).build() {
+					Ok(client) => client,
+					Err(e) => {
+						log_info!(
+							self.logger,
+							"Payjoin Receiver: Unable to construct reqwest client {}",
+							e
+						);
+						return Err(Error::PayjoinReceiverEnrollementFailed);
+					},
+				};
+				let response = match client.get(payjoin_directory).send().await {
+					Ok(response) => response,
+					Err(e) => {
+						log_info!(
+							self.logger,
+							"Payjoin Receiver: Unable to make request to fetch ohttp keys {}",
+							e
+						);
+						return Err(Error::PayjoinReceiverEnrollementFailed);
+					},
+				};
+				if response.status() != reqwest::StatusCode::OK {
+					log_info!(
+						self.logger,
+						"Payjoin Receiver: Got non 200 response when fetching ohttp keys {}",
+						response.status()
+					);
+					return Err(Error::PayjoinReceiverEnrollementFailed);
+				}
+				let response = match response.bytes().await {
+					Ok(response) => response,
+					Err(e) => {
+						log_info!(
+							self.logger,
+							"Payjoin Receiver: Error reading ohttp keys response {}",
+							e
+						);
+						return Err(Error::PayjoinReceiverEnrollementFailed);
+					},
+				};
+				OhttpKeys::decode(response.to_vec().as_slice()).map_err(|e| {
+					log_info!(self.logger, "Payjoin Receiver: Unable to decode ohttp keys {}", e);
+					Error::PayjoinReceiverEnrollementFailed
+				})?
+			},
+		};
+		let mut enroller = Enroller::from_directory_config(
+			self.payjoin_directory.clone(),
+			ohttp_keys.clone(),
+			self.payjoin_relay.clone(),
+		);
+		let (req, ctx) = match enroller.extract_req() {
+			Ok(req) => req,
+			Err(e) => {
+				log_info!(
+					self.logger,
+					"Payjoin Receiver: unable to extract enrollement request {}",
+					e
+				);
+				return Err(Error::PayjoinReceiverEnrollementFailed);
+			},
+		};
+		let response = match reqwest::Client::new()
+			.post(&req.url.to_string())
+			.body(req.body)
+			.headers(ohttp_headers())
+			.send()
+			.await
+		{
+			Ok(response) => response,
+			Err(_) => {
+				log_info!(self.logger, "Payjoin Receiver: unable to make enrollement request");
+				return Err(Error::PayjoinReceiverEnrollementFailed);
+			},
+		};
+		let response = match response.bytes().await {
+			Ok(response) => response,
+			Err(_) => {
+				panic!("Error reading response");
+			},
+		};
+		let enrolled = match enroller.process_res(response.to_vec().as_slice(), ctx) {
+			Ok(enrolled) => enrolled,
+			Err(e) => {
+				log_info!(
+					self.logger,
+					"Payjoin Receiver: unable to process enrollement response {}",
+					e
+				);
+				return Err(Error::PayjoinReceiverEnrollementFailed);
+			},
+		};
+
+		*self.ohttp_keys.write().await = Some(ohttp_keys);
+		*self.enrolled.write().await = Some(enrolled);
+		Ok(())
+	}
+
+	async fn is_enrolled(&self) -> bool {
+		self.enrolled.read().await.deref().is_some()
+			&& self.ohttp_keys.read().await.deref().is_some()
+	}
+
+	/// Schedule a channel to opened upon receiving a Payjoin tranasction value with the same
+	/// channel funding amount.
+	pub(crate) async fn schedule_channel(
+		&self, amount: bitcoin::Amount, counterparty_node_id: bitcoin::secp256k1::PublicKey,
+		channel_id: u128,
+	) {
+		let channel = PayjoinChannel::new(amount, counterparty_node_id, channel_id);
+		self.channel_scheduler.write().await.schedule(
+			channel.channel_value_satoshi(),
+			channel.counterparty_node_id(),
+			channel.channel_id(),
+		);
+	}
+
+	/// This should only be called upon receiving [`Event::FundingTxBroadcastSafe`]
+	///
+	/// [`Event::FundingTxBroadcastSafe`]: lightning::events::Event::FundingTxBroadcastSafe
+	pub(crate) async fn set_funding_tx_signed(
+		&self, funding_tx: Transaction,
+	) -> Option<(payjoin::Url, Vec<u8>)> {
+		self.channel_scheduler.write().await.set_funding_tx_signed(funding_tx)
+	}
+
+	/// Validate an incoming Payjoin request as specified in [BIP78].
+	///
+	/// [BIP78]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki#user-content-Receivers_original_PSBT_checklist
+	async fn validate_payjoin_request(
+		&self, proposal: UncheckedProposal,
+	) -> Result<ProvisionalProposal, Error> {
+		let wallet = &self.wallet;
+		let proposal = proposal.assume_interactive_receiver();
+		let proposal = proposal
+			.check_inputs_not_owned(|script| {
+				Ok(wallet.is_mine(&script.to_owned()).unwrap_or(false))
+			})
+			.map_err(|e| {
+				log_info!(self.logger, "Inputs owned by us check failed {}", e);
+				Error::PayjoinReceiverRequestValidationFailed
+			})?;
+		let proposal = proposal.check_no_mixed_input_scripts().map_err(|e| {
+			log_info!(self.logger, "Mixed input scripts check failed {}", e);
+			Error::PayjoinReceiverRequestValidationFailed
+		})?;
+		// Fixme: discuss how to handle this, instead of the Ok(false) we should have a way to
+		// store seen outpoints and check against them
+		let proposal =
+			proposal.check_no_inputs_seen_before(|_outpoint| Ok(false)).map_err(|e| {
+				log_info!(self.logger, "Inputs seen before check failed {}", e);
+				Error::PayjoinReceiverRequestValidationFailed
+			})?;
+		let provisional_proposal = proposal
+			.identify_receiver_outputs(|script| {
+				Ok(wallet.is_mine(&script.to_owned()).unwrap_or(false))
+			})
+			.map_err(|e| {
+				log_info!(self.logger, "Identify receiver outputs failed {}", e);
+				Error::PayjoinReceiverRequestValidationFailed
+			})?;
+		Ok(provisional_proposal)
+	}
+}
diff --git a/src/payment/mod.rs b/src/payment/mod.rs
index 1862bf2df..aa681a67d 100644
--- a/src/payment/mod.rs
+++ b/src/payment/mod.rs
@@ -3,9 +3,11 @@
 mod bolt11;
 mod bolt12;
 mod onchain;
+pub(crate) mod payjoin;
 mod spontaneous;
 pub(crate) mod store;
 
+pub use self::payjoin::PayjoinPayment;
 pub use bolt11::Bolt11Payment;
 pub use bolt12::Bolt12Payment;
 pub use onchain::OnchainPayment;
diff --git a/src/payment/payjoin/handler.rs b/src/payment/payjoin/handler.rs
new file mode 100644
index 000000000..d9956ad12
--- /dev/null
+++ b/src/payment/payjoin/handler.rs
@@ -0,0 +1,288 @@
+use lightning::ln::channelmanager::PaymentId;
+
+use crate::config::PAYJOIN_REQUEST_TIMEOUT;
+use crate::error::Error;
+use crate::event::PayjoinPaymentFailureReason;
+use crate::io::utils::ohttp_headers;
+use crate::payment::store::PaymentDetailsUpdate;
+use crate::payment::PaymentStatus;
+use crate::types::{ChainSource, EventQueue, PaymentStore, Wallet};
+use crate::Event;
+
+use bitcoin::address::NetworkChecked;
+use bitcoin::block::Header;
+use bitcoin::psbt::Psbt;
+use bitcoin::{Address, Amount, BlockHash, Script, Transaction, Txid};
+use lightning::chain::channelmonitor::ANTI_REORG_DELAY;
+use lightning::chain::transaction::TransactionData;
+use lightning::chain::{Filter, WatchedOutput};
+
+use std::sync::{Arc, RwLock};
+
+#[derive(Clone, Debug)]
+enum PayjoinTransaction {
+	PendingFirstConfirmation {
+		original_psbt: Psbt,
+		tx: Transaction,
+		receiver: Address,
+		amount: Amount,
+	},
+	PendingThresholdConfirmations {
+		original_psbt: Psbt,
+		tx: Transaction,
+		receiver: Address,
+		amount: Amount,
+		first_confirmation_height: u32,
+		first_confirmation_hash: BlockHash,
+	},
+}
+
+impl PayjoinTransaction {
+	fn txid(&self) -> Option<Txid> {
+		match self {
+			PayjoinTransaction::PendingFirstConfirmation { tx, .. } => Some(tx.txid()),
+			PayjoinTransaction::PendingThresholdConfirmations { tx, .. } => Some(tx.txid()),
+		}
+	}
+	fn original_psbt(&self) -> &Psbt {
+		match self {
+			PayjoinTransaction::PendingFirstConfirmation { original_psbt, .. } => original_psbt,
+			PayjoinTransaction::PendingThresholdConfirmations { original_psbt, .. } => {
+				original_psbt
+			},
+		}
+	}
+	fn first_confirmation_height(&self) -> Option<u32> {
+		match self {
+			PayjoinTransaction::PendingFirstConfirmation { .. } => None,
+			PayjoinTransaction::PendingThresholdConfirmations {
+				first_confirmation_height, ..
+			} => Some(*first_confirmation_height),
+		}
+	}
+	fn amount(&self) -> Amount {
+		match self {
+			PayjoinTransaction::PendingFirstConfirmation { amount, .. } => *amount,
+			PayjoinTransaction::PendingThresholdConfirmations { amount, .. } => *amount,
+		}
+	}
+	fn receiver(&self) -> Address {
+		match self {
+			PayjoinTransaction::PendingFirstConfirmation { receiver, .. } => receiver.clone(),
+			PayjoinTransaction::PendingThresholdConfirmations { receiver, .. } => receiver.clone(),
+		}
+	}
+}
+
+pub(crate) struct PayjoinHandler {
+	payjoin_relay: payjoin::Url,
+	chain_source: Arc<ChainSource>,
+	transactions: RwLock<Vec<PayjoinTransaction>>,
+	event_queue: Arc<EventQueue>,
+	wallet: Arc<Wallet>,
+	payment_store: Arc<PaymentStore>,
+}
+
+impl PayjoinHandler {
+	pub(crate) fn new(
+		payjoin_relay: payjoin::Url, chain_source: Arc<ChainSource>, event_queue: Arc<EventQueue>,
+		wallet: Arc<Wallet>, payment_store: Arc<PaymentStore>,
+	) -> Self {
+		Self {
+			payjoin_relay,
+			transactions: RwLock::new(Vec::new()),
+			chain_source,
+			event_queue,
+			wallet,
+			payment_store,
+		}
+	}
+
+	pub(crate) async fn send_request(
+		&self, payjoin_uri: payjoin::Uri<'_, NetworkChecked>, original_psbt: &mut Psbt,
+	) -> Result<Option<Psbt>, Error> {
+		let (request, context) = payjoin::send::RequestBuilder::from_psbt_and_uri(
+			original_psbt.clone(),
+			payjoin_uri.clone(),
+		)
+		.and_then(|b| b.build_non_incentivizing())
+		.and_then(|mut c| c.extract_v2(self.payjoin_relay.clone()))
+		.map_err(|_e| Error::PayjoinRequestCreationFailed)?;
+		let response = reqwest::Client::new()
+			.post(request.url.clone())
+			.body(request.body.clone())
+			.timeout(PAYJOIN_REQUEST_TIMEOUT)
+			.headers(ohttp_headers())
+			.send()
+			.await?;
+		let response = response.error_for_status()?;
+		let response = response.bytes().await?;
+		let response = response.to_vec();
+		context
+			.process_response(&mut response.as_slice())
+			.map_err(|_e| Error::PayjoinResponseProcessingFailed)
+	}
+
+	pub(crate) fn handle_request_failure(
+		&self, payjoin_uri: payjoin::Uri<NetworkChecked>, original_psbt: &Psbt,
+	) -> Result<(), Error> {
+		self.event_queue.add_event(Event::PayjoinPaymentFailed {
+			txid: Some(original_psbt.unsigned_tx.txid()),
+			receipient: payjoin_uri.address.clone().into(),
+			amount: payjoin_uri.amount.unwrap().to_sat(),
+			reason: PayjoinPaymentFailureReason::RequestFailed,
+		})
+	}
+
+	pub(crate) fn handle_request_timeout(
+		&self, payjoin_uri: payjoin::Uri<NetworkChecked>, original_psbt: &Psbt,
+	) -> Result<(), Error> {
+		self.event_queue.add_event(Event::PayjoinPaymentFailed {
+			txid: Some(original_psbt.unsigned_tx.txid()),
+			receipient: payjoin_uri.address.clone().into(),
+			amount: payjoin_uri.amount.unwrap().to_sat(),
+			reason: PayjoinPaymentFailureReason::Timeout,
+		})
+	}
+
+	pub(crate) fn process_response(
+		&self, payjoin_proposal: &mut Psbt, original_psbt: &mut Psbt,
+		payjoin_uri: payjoin::Uri<NetworkChecked>,
+	) -> Result<Transaction, Error> {
+		let wallet = self.wallet.clone();
+		wallet.sign_payjoin_proposal(payjoin_proposal, original_psbt)?;
+		let tx = payjoin_proposal.clone().extract_tx();
+		let our_input =
+			tx.output.iter().find(|output| wallet.is_mine(&output.script_pubkey).unwrap_or(false));
+		if let Some(our_input) = our_input {
+			self.transactions.write().unwrap().push(PayjoinTransaction::PendingFirstConfirmation {
+				original_psbt: original_psbt.clone(),
+				tx: tx.clone(),
+				receiver: payjoin_uri.address.clone(),
+				amount: payjoin_uri.amount.unwrap_or_default(),
+			});
+			let txid = tx.txid();
+			self.register_tx(&txid, &our_input.script_pubkey);
+			self.event_queue.add_event(Event::PayjoinPaymentPending {
+				txid,
+				amount: payjoin_uri.amount.unwrap_or_default().to_sat(),
+				receipient: payjoin_uri.address.clone().into(),
+			})?;
+			Ok(tx)
+		} else {
+			self.event_queue.add_event(Event::PayjoinPaymentFailed {
+				txid: None,
+				amount: payjoin_uri.amount.unwrap_or_default().to_sat(),
+				receipient: payjoin_uri.address.clone().into(),
+				reason: PayjoinPaymentFailureReason::TransactionFinalisationFailed,
+			})?;
+			Err(Error::PayjoinReceiverRequestValidationFailed) // fixeror
+		}
+	}
+
+	fn internal_transactions_confirmed(
+		&self, header: &Header, txdata: &TransactionData, height: u32,
+	) {
+		let (_, tx) = txdata[0];
+		let confirmed_tx_txid = tx.txid();
+		let mut transactions = self.transactions.write().unwrap();
+		let position = match transactions.iter().position(|o| o.txid() == Some(confirmed_tx_txid)) {
+			Some(position) => position,
+			None => {
+				return;
+			},
+		};
+		let pj_tx = transactions.remove(position);
+		match pj_tx {
+			PayjoinTransaction::PendingFirstConfirmation {
+				ref tx,
+				receiver,
+				amount,
+				original_psbt,
+			} => {
+				transactions.push(PayjoinTransaction::PendingThresholdConfirmations {
+					original_psbt,
+					tx: tx.clone(),
+					receiver,
+					amount,
+					first_confirmation_height: height,
+					first_confirmation_hash: header.block_hash(),
+				});
+			},
+			_ => {
+				unreachable!()
+			},
+		};
+	}
+
+	fn internal_get_relevant_txids(&self) -> Vec<(Txid, u32, Option<BlockHash>)> {
+		let state_lock = self.transactions.read().unwrap();
+		state_lock
+			.iter()
+			.filter_map(|o| match o {
+				PayjoinTransaction::PendingThresholdConfirmations {
+					tx,
+					first_confirmation_height,
+					first_confirmation_hash,
+					..
+				} => Some((
+					tx.clone().txid(),
+					first_confirmation_height.clone(),
+					Some(first_confirmation_hash.clone()),
+				)),
+				_ => None,
+			})
+			.collect::<Vec<_>>()
+	}
+
+	fn internal_best_block_updated(&self, height: u32) {
+		let mut transactions = self.transactions.write().unwrap();
+		transactions.retain(|tx| {
+			if let (Some(first_conf), Some(txid)) = (tx.first_confirmation_height(), tx.txid()) {
+				if height - first_conf >= ANTI_REORG_DELAY {
+					let payment_id: [u8; 32] =
+						tx.original_psbt().unsigned_tx.txid()[..].try_into().unwrap();
+					let mut update_details = PaymentDetailsUpdate::new(PaymentId(payment_id));
+					update_details.status = Some(PaymentStatus::Succeeded);
+					let _ = self.payment_store.update(&update_details);
+					let _ = self.event_queue.add_event(Event::PayjoinPaymentSuccess {
+						txid,
+						amount: tx.amount().to_sat(),
+						receipient: tx.receiver().into(),
+					});
+					false
+				} else {
+					true
+				}
+			} else {
+				true
+			}
+		});
+	}
+}
+
+impl Filter for PayjoinHandler {
+	fn register_tx(&self, txid: &Txid, script_pubkey: &Script) {
+		self.chain_source.register_tx(txid, script_pubkey);
+	}
+
+	fn register_output(&self, output: WatchedOutput) {
+		self.chain_source.register_output(output);
+	}
+}
+
+impl lightning::chain::Confirm for PayjoinHandler {
+	fn transactions_confirmed(&self, header: &Header, txdata: &TransactionData, height: u32) {
+		self.internal_transactions_confirmed(header, txdata, height);
+	}
+
+	fn transaction_unconfirmed(&self, _txid: &Txid) {}
+
+	fn best_block_updated(&self, _header: &Header, height: u32) {
+		self.internal_best_block_updated(height);
+	}
+
+	fn get_relevant_txids(&self) -> Vec<(Txid, u32, Option<BlockHash>)> {
+		self.internal_get_relevant_txids()
+	}
+}
diff --git a/src/payment/payjoin/mod.rs b/src/payment/payjoin/mod.rs
new file mode 100644
index 000000000..3c71f61d1
--- /dev/null
+++ b/src/payment/payjoin/mod.rs
@@ -0,0 +1,318 @@
+//! Holds a payment handler allowing to send Payjoin payments.
+
+use lightning::chain::chaininterface::BroadcasterInterface;
+use lightning::ln::channelmanager::PaymentId;
+use lightning::log_error;
+
+use crate::config::{PAYJOIN_REQUEST_TOTAL_DURATION, PAYJOIN_RETRY_INTERVAL};
+use crate::logger::{FilesystemLogger, Logger};
+use crate::types::{Broadcaster, ChannelManager, PaymentStore, Wallet};
+use bitcoin::secp256k1::PublicKey;
+use lightning::ln::msgs::SocketAddress;
+use lightning::util::config::{ChannelHandshakeConfig, UserConfig};
+use payjoin::PjUri;
+
+use crate::connection::ConnectionManager;
+use crate::payjoin_receiver::PayjoinReceiver;
+use crate::peer_store::{PeerInfo, PeerStore};
+use crate::{error::Error, Config};
+
+use std::sync::{Arc, RwLock};
+
+pub(crate) mod handler;
+
+use handler::PayjoinHandler;
+
+use super::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus};
+
+/// A payment handler allowing to send Payjoin payments.
+///
+/// Payjoin transactions can be used to improve privacy by breaking the common-input-ownership
+/// heuristic when Payjoin receivers contribute input(s) to the transaction. They can also be used to
+/// save on fees, as the Payjoin receiver can direct the incoming funds to open a lightning
+/// channel, forwards the funds to another address, or simply consolidate UTXOs.
+///
+/// Payjoin [`BIP77`] implementation. Compatible also with previous Payjoin version [`BIP78`].
+///
+/// Should be retrieved by calling [`Node::payjoin_payment`].
+///
+/// In a Payjoin, both the sender and receiver contribute inputs to the transaction in a
+/// coordinated manner. The Payjoin mechanism is also called pay-to-endpoint(P2EP).
+///
+/// The Payjoin receiver endpoint address is communicated through a [`BIP21`] URI, along with the
+/// payment address and amount.  In the Payjoin process, parties edit, sign and pass iterations of
+/// the transaction between each other, before a final version is broadcasted by the Payjoin
+/// sender. [`BIP77`] codifies a protocol with 2 iterations (or one round of interaction beyond
+/// address sharing).
+///
+/// [`BIP77`] Defines the Payjoin process to happen asynchronously, with the Payjoin receiver
+/// enrolling with a Payjoin Directory to receive Payjoin requests. The Payjoin sender can then
+/// make requests through a proxy server, Payjoin Relay, to the Payjoin receiver even if the
+/// receiver is offline. This mechanism requires the Payjoin sender to regulary check for responses
+/// from the Payjoin receiver as implemented in [`Node::payjoin_payment::send`].
+///
+/// A Payjoin Relay is a proxy server that forwards Payjoin requests from the Payjoin sender to the
+///	Payjoin receiver subdirectory. A Payjoin Relay can be run by anyone. Public Payjoin Relay servers are:
+///	- <https://pj.bobspacebkk.com>
+///
+/// A Payjoin directory is a service that allows Payjoin receivers to receive Payjoin requests
+/// offline. A Payjoin directory can be run by anyone. Public Payjoin Directory servers are:
+/// - <https://payjo.in>
+///
+/// For futher information on Payjoin, please refer to the BIPs included in this documentation. Or
+/// visit the [Payjoin website](https://payjoin.org).
+///
+/// [`Node::payjoin_payment`]: crate::Node::payjoin_payment
+/// [`Node::payjoin_payment::send`]: crate::payment::PayjoinPayment::send
+/// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
+/// [`BIP78`]: https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki
+/// [`BIP77`]: https://github.com/bitcoin/bips/blob/3b863a402e0250658985f08a455a6cd103e269e5/bip-0077.mediawiki
+pub struct PayjoinPayment {
+	runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>,
+	payjoin_handler: Option<Arc<PayjoinHandler>>,
+	receiver: Option<Arc<PayjoinReceiver>>,
+	config: Arc<Config>,
+	logger: Arc<FilesystemLogger>,
+	wallet: Arc<Wallet>,
+	tx_broadcaster: Arc<Broadcaster>,
+	peer_store: Arc<PeerStore<Arc<FilesystemLogger>>>,
+	channel_manager: Arc<ChannelManager>,
+	connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
+	payment_store: Arc<PaymentStore>,
+}
+
+impl PayjoinPayment {
+	pub(crate) fn new(
+		runtime: Arc<RwLock<Option<tokio::runtime::Runtime>>>,
+		payjoin_handler: Option<Arc<PayjoinHandler>>, receiver: Option<Arc<PayjoinReceiver>>,
+		config: Arc<Config>, logger: Arc<FilesystemLogger>, wallet: Arc<Wallet>,
+		tx_broadcaster: Arc<Broadcaster>, peer_store: Arc<PeerStore<Arc<FilesystemLogger>>>,
+		channel_manager: Arc<ChannelManager>,
+		connection_manager: Arc<ConnectionManager<Arc<FilesystemLogger>>>,
+		payment_store: Arc<PaymentStore>,
+	) -> Self {
+		Self {
+			runtime,
+			payjoin_handler,
+			receiver,
+			config,
+			logger,
+			wallet,
+			tx_broadcaster,
+			peer_store,
+			channel_manager,
+			connection_manager,
+			payment_store,
+		}
+	}
+
+	/// Send a Payjoin transaction to the address specified in the `payjoin_uri`.
+	///
+	/// The `payjoin_uri` argument is expected to be a valid [`BIP21`] URI with Payjoin parameters
+	/// set.
+	///
+	/// Due to the asynchronous nature of the Payjoin process, this method will return immediately
+	/// after constucting the Payjoin request and sending it in the background. The result of the
+	/// operation will be communicated through the event queue. If the Payjoin request is
+	/// successful, [`Event::PayjoinTxSendSuccess`] event will be added to the event queue.
+	/// Otherwise, [`Event::PayjoinTxSendFailed`] is added.
+	///
+	/// The total duration of the Payjoin process is defined in `PAYJOIN_REQUEST_TOTAL_DURATION`.
+	/// If the Payjoin receiver does not respond within this duration, the process is considered
+	/// failed. Note, the Payjoin receiver can still broadcast the original PSBT shared with them as
+	/// part of our request in a regular transaction if we timed out, or for any other reason. The
+	/// Payjoin sender should monitor the blockchain for such transactions and handle them
+	/// accordingly.
+	///
+	/// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
+	/// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki
+	/// [`Event::PayjoinTxSendSuccess`]: crate::Event::PayjoinTxSendSuccess
+	/// [`Event::PayjoinTxSendFailed`]: crate::Event::PayjoinTxSendFailed
+	pub fn send(&self, payjoin_uri: String) -> Result<(), Error> {
+		let rt_lock = self.runtime.read().unwrap();
+		if rt_lock.is_none() {
+			return Err(Error::NotRunning);
+		}
+		let payjoin_handler = self.payjoin_handler.as_ref().ok_or(Error::PayjoinUnavailable)?;
+		let payjoin_uri =
+			payjoin::Uri::try_from(payjoin_uri).map_err(|_| Error::PayjoinUriInvalid).and_then(
+				|uri| uri.require_network(self.config.network).map_err(|_| Error::InvalidNetwork),
+			)?;
+		let original_psbt = self.wallet.build_payjoin_transaction(payjoin_uri.clone())?;
+		let payjoin_handler = Arc::clone(payjoin_handler);
+		let runtime = rt_lock.as_ref().unwrap();
+		let tx_broadcaster = Arc::clone(&self.tx_broadcaster);
+		let logger = Arc::clone(&self.logger);
+		let payment_store = Arc::clone(&self.payment_store);
+		let payment_id = original_psbt.unsigned_tx.txid()[..].try_into().unwrap();
+		payment_store.insert(PaymentDetails::new(
+			PaymentId(payment_id),
+			PaymentKind::Payjoin,
+			payjoin_uri.amount.map(|a| a.to_sat()),
+			PaymentDirection::Outbound,
+			PaymentStatus::Pending,
+		))?;
+		runtime.spawn(async move {
+			let mut interval = tokio::time::interval(PAYJOIN_RETRY_INTERVAL);
+			loop {
+				tokio::select! {
+					_ = tokio::time::sleep(PAYJOIN_REQUEST_TOTAL_DURATION) => {
+						let _ = payjoin_handler.handle_request_timeout(payjoin_uri.clone(), &original_psbt);
+						break;
+					}
+					_ = interval.tick() => {
+						let payjoin_uri = payjoin_uri.clone();
+						match payjoin_handler.send_request(payjoin_uri.clone(), &mut original_psbt.clone()).await {
+							Ok(Some(mut proposal)) => {
+								let _ = payjoin_handler.process_response(&mut proposal, &mut original_psbt.clone(), payjoin_uri).inspect(|tx| {
+									tx_broadcaster.broadcast_transactions(&[&tx]);
+								}).inspect_err(|e| {
+									log_error!(logger, "Failed to process Payjoin response: {}", e);
+								});
+								break;
+							},
+							Ok(None) => {
+								continue;
+							}
+							Err(e) => {
+								log_error!(logger, "Failed to send Payjoin request : {}", e);
+								let _ = payjoin_handler.handle_request_failure(payjoin_uri.clone(), &original_psbt);
+								break;
+							},
+						}
+					}
+				}
+			}
+		});
+		return Ok(());
+	}
+
+	/// Send a Payjoin transaction to the address specified in the `payjoin_uri`.
+	///
+	/// The `payjoin_uri` argument is expected to be a valid [`BIP21`] URI with Payjoin parameters
+	/// set.
+	///
+	/// This method will ignore the amount specified in the `payjoin_uri` and use the `amount_sats`
+	/// instead. The `amount_sats` argument is expected to be in satoshis.
+	///
+	/// Due to the asynchronous nature of the Payjoin process, this method will return immediately
+	/// after constucting the Payjoin request and sending it in the background. The result of the
+	/// operation will be communicated through the event queue. If the Payjoin request is
+	/// successful, [`Event::PayjoinTxSendSuccess`] event will be added to the event queue.
+	/// Otherwise, [`Event::PayjoinTxSendFailed`] is added.
+	///
+	/// The total duration of the Payjoin process is defined in `PAYJOIN_REQUEST_TOTAL_DURATION`.
+	/// If the Payjoin receiver does not respond within this duration, the process is considered
+	/// failed. Note, the Payjoin receiver can still broadcast the original PSBT shared with them as
+	/// part of our request in a regular transaction if we timed out, or for any other reason. The
+	/// Payjoin sender should monitor the blockchain for such transactions and handle them
+	/// accordingly.
+	///
+	/// [`BIP21`]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
+	/// [`BIP77`]: https://github.com/bitcoin/bips/blob/d7ffad81e605e958dcf7c2ae1f4c797a8631f146/bip-0077.mediawiki
+	/// [`Event::PayjoinTxSendSuccess`]: crate::Event::PayjoinTxSendSuccess
+	/// [`Event::PayjoinTxSendFailed`]: crate::Event::PayjoinTxSendFailed
+	pub fn send_with_amount(&self, payjoin_uri: String, amount_sats: u64) -> Result<(), Error> {
+		let mut payjoin_uri =
+			payjoin::Uri::try_from(payjoin_uri).map_err(|_| Error::PayjoinUriInvalid).and_then(
+				|uri| uri.require_network(self.config.network).map_err(|_| Error::InvalidNetwork),
+			)?;
+		payjoin_uri.amount = Some(bitcoin::Amount::from_sat(amount_sats));
+		self.send(payjoin_uri.to_string())
+	}
+
+	/// Receive onchain Payjoin transaction.
+	///
+	/// This method will enroll with the configured Payjoin directory if not already,
+	/// and returns a [BIP21] URI pointing to our enrolled subdirectory that you can share with
+	/// Payjoin sender.
+	///
+	/// [BIP21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
+	pub fn receive(&self, amount: bitcoin::Amount) -> Result<PjUri, Error> {
+		let rt_lock = self.runtime.read().unwrap();
+		if rt_lock.is_none() {
+			return Err(Error::NotRunning);
+		}
+		if let Some(receiver) = &self.receiver {
+			let runtime = rt_lock.as_ref().unwrap();
+			runtime.handle().block_on(async { receiver.receive(amount).await })
+		} else {
+			Err(Error::PayjoinReceiverUnavailable)
+		}
+	}
+
+	/// Receive on chain Payjoin transaction and open a channel in a single transaction.
+	///
+	/// This method will enroll with the configured Payjoin directory if not already,
+	/// and before returning a [BIP21] URI pointing to our enrolled subdirectory to share with
+	/// Payjoin sender, we start the channel opening process and halt it when we receive
+	/// `accept_channel` from counterparty node. Once the Payjoin request is received, we move
+	/// forward with the channel opening process.
+	///
+	/// [BIP21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki
+	pub fn receive_with_channel_opening(
+		&self, channel_amount_sats: u64, push_msat: Option<u64>, announce_channel: bool,
+		node_id: PublicKey, address: SocketAddress,
+	) -> Result<PjUri, Error> {
+		use rand::Rng;
+		let rt_lock = self.runtime.read().unwrap();
+		if rt_lock.is_none() {
+			return Err(Error::NotRunning);
+		}
+		if let Some(receiver) = &self.receiver {
+			let user_channel_id: u128 = rand::thread_rng().gen::<u128>();
+			let runtime = rt_lock.as_ref().unwrap();
+			runtime.handle().block_on(async {
+				receiver
+					.schedule_channel(
+						bitcoin::Amount::from_sat(channel_amount_sats),
+						node_id,
+						user_channel_id,
+					)
+					.await;
+			});
+			let user_config = UserConfig {
+				channel_handshake_limits: Default::default(),
+				channel_handshake_config: ChannelHandshakeConfig {
+					announced_channel: announce_channel,
+					..Default::default()
+				},
+				..Default::default()
+			};
+			let push_msat = push_msat.unwrap_or(0);
+			let peer_info = PeerInfo { node_id, address };
+
+			let con_node_id = peer_info.node_id;
+			let con_addr = peer_info.address.clone();
+			let con_cm = Arc::clone(&self.connection_manager);
+
+			runtime.handle().block_on(async {
+				let _ = con_cm.connect_peer_if_necessary(con_node_id, con_addr).await;
+			});
+
+			match self.channel_manager.create_channel(
+				peer_info.node_id,
+				channel_amount_sats,
+				push_msat,
+				user_channel_id,
+				None,
+				Some(user_config),
+			) {
+				Ok(_) => {
+					self.peer_store.add_peer(peer_info)?;
+				},
+				Err(_) => {
+					return Err(Error::ChannelCreationFailed);
+				},
+			};
+
+			runtime.handle().block_on(async {
+				let payjoin_uri =
+					receiver.receive(bitcoin::Amount::from_sat(channel_amount_sats)).await?;
+				Ok(payjoin_uri)
+			})
+		} else {
+			Err(Error::PayjoinReceiverUnavailable)
+		}
+	}
+}
diff --git a/src/payment/payjoin/send.rs b/src/payment/payjoin/send.rs
new file mode 100644
index 000000000..f981b3ec2
--- /dev/null
+++ b/src/payment/payjoin/send.rs
@@ -0,0 +1,76 @@
+use crate::config::{PAYJOIN_REQUEST_TIMEOUT, PAYJOIN_RETRY_INTERVAL};
+use crate::io::utils::ohttp_headers;
+use crate::logger::FilesystemLogger;
+
+use lightning::util::logger::Logger;
+use lightning::{log_error, log_info};
+
+use std::sync::Arc;
+
+pub(crate) struct PayjoinSender {
+	logger: Arc<FilesystemLogger>,
+	payjoin_relay: payjoin::Url,
+}
+
+impl PayjoinSender {
+	pub(crate) fn new(logger: Arc<FilesystemLogger>, payjoin_relay: payjoin::Url) -> Self {
+		Self { logger, payjoin_relay }
+	}
+
+	pub(crate) fn payjoin_relay(&self) -> &payjoin::Url {
+		&self.payjoin_relay
+	}
+
+	pub(crate) async fn send_request(&self, request: &payjoin::Request) -> Option<Vec<u8>> {
+		let response = match reqwest::Client::new()
+			.post(request.url.clone())
+			.body(request.body.clone())
+			.timeout(PAYJOIN_REQUEST_TIMEOUT)
+			.headers(ohttp_headers())
+			.send()
+			.await
+		{
+			Ok(response) => response,
+			Err(e) => {
+				log_error!(
+					self.logger,
+					"Error trying to poll Payjoin response: {}, retrying in {} seconds",
+					e,
+					PAYJOIN_RETRY_INTERVAL.as_secs()
+				);
+				return None;
+			},
+		};
+		if response.status() == reqwest::StatusCode::OK {
+			match response.bytes().await.and_then(|r| Ok(r.to_vec())) {
+				Ok(response) => {
+					if response.is_empty() {
+						log_info!(
+						self.logger,
+						"Got empty response while polling Payjoin response, retrying in {} seconds", PAYJOIN_RETRY_INTERVAL.as_secs()
+				);
+						return None;
+					}
+					return Some(response);
+				},
+				Err(e) => {
+					log_error!(
+						self.logger,
+						"Error reading polling Payjoin response: {}, retrying in {} seconds",
+						e,
+						PAYJOIN_RETRY_INTERVAL.as_secs()
+					);
+					return None;
+				},
+			};
+		} else {
+			log_info!(
+				self.logger,
+				"Got status code {} while polling Payjoin response, retrying in {} seconds",
+				response.status(),
+				PAYJOIN_RETRY_INTERVAL.as_secs()
+			);
+			return None;
+		}
+	}
+}
diff --git a/src/payment/store.rs b/src/payment/store.rs
index eb3ac091f..3f0481d9b 100644
--- a/src/payment/store.rs
+++ b/src/payment/store.rs
@@ -232,6 +232,8 @@ pub enum PaymentKind {
 		/// The pre-image used by the payment.
 		preimage: Option<PaymentPreimage>,
 	},
+	/// A Payjoin payment.
+	Payjoin,
 }
 
 impl_writeable_tlv_based_enum!(PaymentKind,
@@ -261,7 +263,8 @@ impl_writeable_tlv_based_enum!(PaymentKind,
 		(0, hash, option),
 		(2, preimage, option),
 		(4, secret, option),
-	};
+	},
+	(12, Payjoin) => {};
 );
 
 /// Limits applying to how much fee we allow an LSP to deduct from the payment amount.
diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs
index 4492bcfc6..2a3867ebc 100644
--- a/src/tx_broadcaster.rs
+++ b/src/tx_broadcaster.rs
@@ -114,8 +114,10 @@ where
 {
 	fn broadcast_transactions(&self, txs: &[&Transaction]) {
 		let package = txs.iter().map(|&t| t.clone()).collect::<Vec<Transaction>>();
-		self.queue_sender.try_send(package).unwrap_or_else(|e| {
+		let ret = self.queue_sender.try_send(package).unwrap_or_else(|e| {
+			dbg!(&e);
 			log_error!(self.logger, "Failed to broadcast transactions: {}", e);
 		});
+		dbg!(&ret);
 	}
 }
diff --git a/src/types.rs b/src/types.rs
index 0c2faeb78..ad21e2310 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -72,6 +72,8 @@ pub(crate) type Wallet = crate::wallet::Wallet<
 	Arc<FilesystemLogger>,
 >;
 
+pub(crate) type EventQueue = crate::event::EventQueue<Arc<FilesystemLogger>>;
+
 pub(crate) type KeysManager = crate::wallet::WalletKeysManager<
 	bdk::database::SqliteDatabase,
 	Arc<Broadcaster>,
@@ -140,6 +142,8 @@ pub(crate) type BumpTransactionEventHandler =
 		Arc<FilesystemLogger>,
 	>;
 
+pub(crate) type PaymentStore = crate::payment::store::PaymentStore<Arc<FilesystemLogger>>;
+
 /// A local, potentially user-provided, identifier of a channel.
 ///
 /// By default, this will be randomly generated for the user to ensure local uniqueness.
diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs
index 9dd7e5699..f5fccbc20 100644
--- a/src/uniffi_types.rs
+++ b/src/uniffi_types.rs
@@ -1,3 +1,4 @@
+pub use crate::event::PayjoinPaymentFailureReason;
 pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo};
 pub use crate::payment::store::{LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus};
 
@@ -11,7 +12,7 @@ pub use lightning::util::string::UntrustedString;
 
 pub use lightning_invoice::Bolt11Invoice;
 
-pub use bitcoin::{Address, BlockHash, Network, OutPoint, Txid};
+pub use bitcoin::{Address, BlockHash, Network, OutPoint, ScriptBuf, Txid};
 
 pub use bip39::Mnemonic;
 
@@ -31,6 +32,18 @@ use lightning_invoice::SignedRawBolt11Invoice;
 use std::convert::TryInto;
 use std::str::FromStr;
 
+impl UniffiCustomTypeConverter for ScriptBuf {
+	type Builtin = String;
+
+	fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> {
+		Ok(ScriptBuf::from_hex(&val).map_err(|_| Error::InvalidPublicKey)?)
+	}
+
+	fn from_custom(obj: Self) -> Self::Builtin {
+		obj.to_hex_string()
+	}
+}
+
 impl UniffiCustomTypeConverter for PublicKey {
 	type Builtin = String;
 
diff --git a/src/wallet.rs b/src/wallet.rs
index 0da3f6db8..c8e5ddb23 100644
--- a/src/wallet.rs
+++ b/src/wallet.rs
@@ -3,6 +3,7 @@ use crate::logger::{log_error, log_info, log_trace, Logger};
 use crate::config::BDK_WALLET_SYNC_TIMEOUT_SECS;
 use crate::Error;
 
+use bitcoin::psbt::Psbt;
 use lightning::chain::chaininterface::{BroadcasterInterface, ConfirmationTarget, FeeEstimator};
 
 use lightning::events::bump_transaction::{Utxo, WalletSource};
@@ -18,10 +19,10 @@ use lightning::util::message_signing;
 use bdk::blockchain::EsploraBlockchain;
 use bdk::database::BatchDatabase;
 use bdk::wallet::AddressIndex;
-use bdk::{Balance, FeeRate};
+use bdk::{Balance, FeeRate, LocalUtxo};
 use bdk::{SignOptions, SyncOptions};
 
-use bitcoin::address::{Payload, WitnessVersion};
+use bitcoin::address::{NetworkChecked, Payload, WitnessVersion};
 use bitcoin::bech32::u5;
 use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR;
 use bitcoin::blockdata::locktime::absolute::LockTime;
@@ -34,6 +35,7 @@ use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature};
 use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing};
 use bitcoin::{ScriptBuf, Transaction, TxOut, Txid};
 
+use std::collections::{BTreeMap, HashMap};
 use std::ops::{Deref, DerefMut};
 use std::sync::{Arc, Mutex, RwLock};
 use std::time::Duration;
@@ -149,6 +151,117 @@ where
 		res
 	}
 
+	// Returns the total value of all outputs in the given transaction that are directed to us
+	pub(crate) fn funds_directed_to_us(&self, tx: &Transaction) -> Result<bitcoin::Amount, Error> {
+		let locked_wallet = self.inner.lock().unwrap();
+		let total_value = tx.output.iter().fold(0, |acc, output| {
+			match locked_wallet.is_mine(&output.script_pubkey) {
+				Ok(true) => acc + output.value,
+				_ => acc,
+			}
+		});
+		Ok(bitcoin::Amount::from_sat(total_value))
+	}
+
+	pub(crate) fn build_payjoin_transaction(
+		&self, payjoin_uri: payjoin::Uri<NetworkChecked>,
+	) -> Result<Psbt, Error> {
+		let locked_wallet = self.inner.lock().unwrap();
+		let network = locked_wallet.network();
+		let output_script = payjoin_uri.address.script_pubkey();
+		let amount = payjoin_uri.amount.ok_or(Error::PayjoinRequestMissingAmount)?.to_sat();
+		let fee_rate = match network {
+			bitcoin::Network::Regtest => 1000.0,
+			_ => self
+				.fee_estimator
+				.get_est_sat_per_1000_weight(ConfirmationTarget::OutputSpendingFee) as f32,
+		};
+		let fee_rate = FeeRate::from_sat_per_kwu(fee_rate);
+		let mut tx_builder = locked_wallet.build_tx();
+		tx_builder.add_recipient(output_script, amount).fee_rate(fee_rate).enable_rbf();
+		let mut psbt = match tx_builder.finish() {
+			Ok((psbt, _)) => {
+				log_trace!(self.logger, "Created Payjoin transaction: {:?}", psbt);
+				psbt
+			},
+			Err(err) => {
+				log_error!(self.logger, "Failed to create Payjoin transaction: {}", err);
+				return Err(err.into());
+			},
+		};
+		locked_wallet.sign(&mut psbt, SignOptions::default())?;
+		Ok(psbt)
+	}
+
+	pub(crate) fn sign_payjoin_proposal(
+		&self, payjoin_proposal_psbt: &mut Psbt, original_psbt: &mut Psbt,
+	) -> Result<bool, Error> {
+		// BDK only signs scripts that match its target descriptor by iterating through input map.
+		// The BIP 78 spec makes receiver clear sender input map UTXOs, so process_response will
+		// fail unless they're cleared.  A PSBT unsigned_tx.input references input OutPoints and
+		// not a Script, so the sender signer must either be able to sign based on OutPoint UTXO
+		// lookup or otherwise re-introduce the Script from original_psbt.  Since BDK PSBT signer
+		// only checks Input map Scripts for match against its descriptor, it won't sign if they're
+		// empty.  Re-add the scripts from the original_psbt in order for BDK to sign properly.
+		// reference: https://github.com/bitcoindevkit/bdk-cli/pull/156#discussion_r1261300637
+		let mut original_inputs =
+			original_psbt.unsigned_tx.input.iter().zip(&mut original_psbt.inputs).peekable();
+		for (proposed_txin, proposed_psbtin) in
+			payjoin_proposal_psbt.unsigned_tx.input.iter().zip(&mut payjoin_proposal_psbt.inputs)
+		{
+			if let Some((original_txin, original_psbtin)) = original_inputs.peek() {
+				if proposed_txin.previous_output == original_txin.previous_output {
+					proposed_psbtin.witness_utxo = original_psbtin.witness_utxo.clone();
+					proposed_psbtin.non_witness_utxo = original_psbtin.non_witness_utxo.clone();
+					original_inputs.next();
+				}
+			}
+		}
+		let wallet = self.inner.lock().unwrap();
+		let is_signed = wallet.sign(payjoin_proposal_psbt, SignOptions::default())?;
+		Ok(is_signed)
+	}
+
+	// Returns a list of unspent outputs that can be used as inputs to improve the privacy of a
+	// payjoin transaction.
+	pub(crate) fn payjoin_receiver_candidate_input(
+		&self,
+	) -> Result<(HashMap<bitcoin::Amount, bitcoin::OutPoint>, Vec<LocalUtxo>), Error> {
+		let locked_wallet = self.inner.lock().unwrap();
+		let utxo_set = locked_wallet.list_unspent()?;
+		let candidate_inputs = utxo_set
+			.iter()
+			.filter_map(|utxo| {
+				if !utxo.is_spent {
+					Some((bitcoin::Amount::from_sat(utxo.txout.value), utxo.outpoint))
+				} else {
+					None
+				}
+			})
+			.collect();
+		Ok((candidate_inputs, utxo_set))
+	}
+
+	pub(crate) fn prepare_payjoin_proposal(&self, mut psbt: Psbt) -> Result<Psbt, Error> {
+		let wallet = self.inner.lock().unwrap();
+		let mut sign_options = SignOptions::default();
+		sign_options.trust_witness_utxo = true;
+		wallet.sign(&mut psbt, sign_options)?;
+		// Clear derivation paths from the PSBT as required by BIP78/BIP77
+		psbt.inputs.iter_mut().for_each(|i| {
+			i.bip32_derivation = BTreeMap::new();
+		});
+		psbt.outputs.iter_mut().for_each(|o| {
+			o.bip32_derivation = BTreeMap::new();
+		});
+		Ok(psbt)
+	}
+
+	pub(crate) fn is_mine(&self, script: &ScriptBuf) -> Result<bool, Error> {
+		let locked_wallet = self.inner.lock().unwrap();
+		Ok(locked_wallet.is_mine(script)?)
+	}
+
 	pub(crate) fn create_funding_transaction(
 		&self, output_script: ScriptBuf, value_sats: u64, confirmation_target: ConfirmationTarget,
 		locktime: LockTime,
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index 5959bd58e..edccad541 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -147,6 +147,40 @@ macro_rules! expect_payment_successful_event {
 
 pub(crate) use expect_payment_successful_event;
 
+macro_rules! expect_payjoin_tx_pending_event {
+	($node: expr) => {{
+		match $node.wait_next_event() {
+			ref e @ Event::PayjoinPaymentPending { txid, .. } => {
+				println!("{} got event {:?}", $node.node_id(), e);
+				$node.event_handled();
+				txid
+			},
+			ref e => {
+				panic!("{} got unexpected event!: {:?}", std::stringify!($node), e);
+			},
+		}
+	}};
+}
+
+pub(crate) use expect_payjoin_tx_pending_event;
+
+macro_rules! expect_payjoin_tx_sent_successfully_event {
+	($node: expr) => {{
+		match $node.wait_next_event() {
+			ref e @ Event::PayjoinPaymentSuccess { txid, .. } => {
+				println!("{} got event {:?}", $node.node_id(), e);
+				$node.event_handled();
+				txid
+			},
+			ref e => {
+				panic!("{} got unexpected event!: {:?}", std::stringify!($node), e);
+			},
+		}
+	}};
+}
+
+pub(crate) use expect_payjoin_tx_sent_successfully_event;
+
 pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) {
 	let bitcoind_exe =
 		env::var("BITCOIND_EXE").ok().or_else(|| bitcoind::downloaded_exe_path().ok()).expect(
@@ -258,6 +292,22 @@ pub(crate) fn setup_two_nodes(
 	(node_a, node_b)
 }
 
+pub(crate) fn setup_two_payjoin_nodes(
+	electrsd: &ElectrsD, allow_0conf: bool,
+) -> (TestNode, TestNode) {
+	println!("== Node A ==");
+	let config_a = random_config(false);
+	let node_a_payjoin_receiver = setup_payjoin_node(electrsd, config_a);
+
+	println!("\n== Node B ==");
+	let mut config_b = random_config(false);
+	if allow_0conf {
+		config_b.trusted_peers_0conf.push(node_a_payjoin_receiver.node_id());
+	}
+	let node_b_payjoin_sender = setup_payjoin_node(electrsd, config_b);
+	(node_a_payjoin_receiver, node_b_payjoin_sender)
+}
+
 pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode {
 	let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
 	setup_builder!(builder, config);
@@ -270,6 +320,21 @@ pub(crate) fn setup_node(electrsd: &ElectrsD, config: Config) -> TestNode {
 	node
 }
 
+pub(crate) fn setup_payjoin_node(electrsd: &ElectrsD, config: Config) -> TestNode {
+	let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());
+	setup_builder!(builder, config);
+	builder.set_esplora_server(esplora_url.clone());
+	let payjoin_directory = "https://payjo.in".to_string();
+	let payjoin_relay = "https://pj.bobspacebkk.com".to_string();
+	builder.set_payjoin_config(payjoin_directory, payjoin_relay, None).unwrap();
+	let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into()));
+	let node = builder.build_with_store(test_sync_store).unwrap();
+	node.start().unwrap();
+	assert!(node.status().is_running);
+	assert!(node.status().latest_fee_rate_cache_update_timestamp.is_some());
+	node
+}
+
 pub(crate) fn generate_blocks_and_wait<E: ElectrumApi>(
 	bitcoind: &BitcoindClient, electrs: &E, num: usize,
 ) {
diff --git a/tests/integration_tests_payjoin.rs b/tests/integration_tests_payjoin.rs
new file mode 100644
index 000000000..43ec91c8f
--- /dev/null
+++ b/tests/integration_tests_payjoin.rs
@@ -0,0 +1,93 @@
+mod common;
+
+use common::{
+	expect_payjoin_tx_sent_successfully_event, generate_blocks_and_wait,
+	premine_and_distribute_funds, setup_bitcoind_and_electrsd, setup_two_payjoin_nodes,
+	wait_for_tx,
+};
+
+use bitcoin::Amount;
+use ldk_node::{
+	payment::{PaymentDirection, PaymentKind, PaymentStatus},
+	Event,
+};
+
+use crate::common::expect_payjoin_tx_pending_event;
+
+#[test]
+fn send_receive_regular_payjoin_transaction() {
+	let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
+	let (node_a_pj_receiver, node_b_pj_sender) = setup_two_payjoin_nodes(&electrsd, false);
+	let addr_b = node_b_pj_sender.onchain_payment().new_address().unwrap();
+	let addr_a = node_a_pj_receiver.onchain_payment().new_address().unwrap();
+	let premine_amount_sat = 100_000_00;
+	premine_and_distribute_funds(
+		&bitcoind.client,
+		&electrsd.client,
+		vec![addr_b, addr_a],
+		Amount::from_sat(premine_amount_sat),
+	);
+	node_a_pj_receiver.sync_wallets().unwrap();
+	node_b_pj_sender.sync_wallets().unwrap();
+	assert_eq!(node_b_pj_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat);
+	assert_eq!(node_a_pj_receiver.list_balances().spendable_onchain_balance_sats, 100_000_00);
+	assert_eq!(node_a_pj_receiver.next_event(), None);
+	let payjoin_payment = node_a_pj_receiver.payjoin_payment();
+	let payjoin_uri = payjoin_payment.receive(Amount::from_sat(80_000)).unwrap();
+	let payjoin_uri = payjoin_uri.to_string();
+	dbg!(&payjoin_uri);
+	let sender_payjoin_payment = node_b_pj_sender.payjoin_payment();
+	assert!(sender_payjoin_payment.send(payjoin_uri).is_ok());
+	let txid = expect_payjoin_tx_pending_event!(node_b_pj_sender);
+	let payments = node_b_pj_sender.list_payments();
+	let payment = payments.first().unwrap();
+	assert_eq!(payment.amount_msat, Some(80_000));
+	assert_eq!(payment.status, PaymentStatus::Pending);
+	assert_eq!(payment.direction, PaymentDirection::Outbound);
+	assert_eq!(payment.kind, PaymentKind::Payjoin);
+	wait_for_tx(&electrsd.client, txid);
+	generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 3);
+	node_b_pj_sender.sync_wallets().unwrap();
+	generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 4);
+	node_b_pj_sender.sync_wallets().unwrap();
+	let payments = node_b_pj_sender.list_payments();
+	let payment = payments.first().unwrap();
+	assert_eq!(payment.status, PaymentStatus::Succeeded);
+	expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender);
+	let node_b_balance = node_b_pj_sender.list_balances();
+	assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000);
+}
+
+#[test]
+fn send_payjoin_with_amount() {
+	let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
+	let (node_a_pj_receiver, node_b_pj_sender) = setup_two_payjoin_nodes(&electrsd, false);
+	let addr_b = node_b_pj_sender.onchain_payment().new_address().unwrap();
+	let addr_a = node_a_pj_receiver.onchain_payment().new_address().unwrap();
+	let premine_amount_sat = 100_000_00;
+	premine_and_distribute_funds(
+		&bitcoind.client,
+		&electrsd.client,
+		vec![addr_b, addr_a],
+		Amount::from_sat(premine_amount_sat),
+	);
+	node_a_pj_receiver.sync_wallets().unwrap();
+	node_b_pj_sender.sync_wallets().unwrap();
+	assert_eq!(node_b_pj_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat);
+	assert_eq!(node_a_pj_receiver.list_balances().spendable_onchain_balance_sats, 100_000_00);
+	assert_eq!(node_a_pj_receiver.next_event(), None);
+	let payjoin_payment = node_a_pj_receiver.payjoin_payment();
+	let payjoin_uri = payjoin_payment.receive(Amount::from_sat(100_000_000)).unwrap();
+	let payjoin_uri = payjoin_uri.to_string();
+	dbg!(&payjoin_uri);
+	let sender_payjoin_payment = node_b_pj_sender.payjoin_payment();
+	assert!(sender_payjoin_payment.send_with_amount(payjoin_uri, 80_000).is_ok());
+	let _txid = expect_payjoin_tx_pending_event!(node_b_pj_sender);
+	generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 3);
+	node_b_pj_sender.sync_wallets().unwrap();
+	generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 4);
+	node_b_pj_sender.sync_wallets().unwrap();
+	let _txid = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender);
+	let node_b_balance = node_b_pj_sender.list_balances();
+	assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000);
+}
diff --git a/tests/integration_tests_payjoin_with_channel_opening.rs b/tests/integration_tests_payjoin_with_channel_opening.rs
new file mode 100644
index 000000000..5e1ad4fa4
--- /dev/null
+++ b/tests/integration_tests_payjoin_with_channel_opening.rs
@@ -0,0 +1,77 @@
+mod common;
+
+use common::{
+	expect_channel_pending_event, expect_channel_ready_event,
+	expect_payjoin_tx_sent_successfully_event, generate_blocks_and_wait,
+	premine_and_distribute_funds, setup_bitcoind_and_electrsd, setup_two_payjoin_nodes,
+	wait_for_tx,
+};
+
+use bitcoin::Amount;
+use ldk_node::Event;
+
+use crate::common::expect_payjoin_tx_pending_event;
+
+#[test]
+fn send_receive_payjoin_transaction_with_channel_opening() {
+	let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();
+	let (node_a_pj_receiver, node_b_pj_sender) = setup_two_payjoin_nodes(&electrsd, false);
+	let addr_b = node_b_pj_sender.onchain_payment().new_address().unwrap();
+	let addr_a = node_a_pj_receiver.onchain_payment().new_address().unwrap();
+	let premine_amount_sat = 100_000_00;
+	premine_and_distribute_funds(
+		&bitcoind.client,
+		&electrsd.client,
+		vec![addr_b, addr_a],
+		Amount::from_sat(premine_amount_sat),
+	);
+	node_a_pj_receiver.sync_wallets().unwrap();
+	node_b_pj_sender.sync_wallets().unwrap();
+	assert_eq!(node_b_pj_sender.list_balances().spendable_onchain_balance_sats, premine_amount_sat);
+	assert_eq!(node_a_pj_receiver.list_balances().spendable_onchain_balance_sats, 100_000_00);
+	assert_eq!(node_a_pj_receiver.next_event(), None);
+	assert_eq!(node_a_pj_receiver.list_channels().len(), 0);
+	let payjoin_payment = node_a_pj_receiver.payjoin_payment();
+	let node_b_listening_address =
+		node_b_pj_sender.listening_addresses().unwrap().get(0).unwrap().clone();
+	let payjoin_uri = payjoin_payment
+		.receive_with_channel_opening(
+			80_000,
+			None,
+			false,
+			node_b_pj_sender.node_id(),
+			node_b_listening_address,
+		)
+		.unwrap();
+	let payjoin_uri = payjoin_uri.to_string();
+	let sender_payjoin_payment = node_b_pj_sender.payjoin_payment();
+	assert!(sender_payjoin_payment.send(payjoin_uri).is_ok());
+	expect_channel_pending_event!(node_a_pj_receiver, node_b_pj_sender.node_id());
+	expect_channel_pending_event!(node_b_pj_sender, node_a_pj_receiver.node_id());
+	let txid = expect_payjoin_tx_pending_event!(node_b_pj_sender);
+	wait_for_tx(&electrsd.client, txid);
+	generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 1);
+	node_b_pj_sender.sync_wallets().unwrap();
+	generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6);
+	node_a_pj_receiver.sync_wallets().unwrap();
+	node_b_pj_sender.sync_wallets().unwrap();
+	let node_b_balance = node_b_pj_sender.list_balances();
+	assert!(node_b_balance.total_onchain_balance_sats < premine_amount_sat - 80000);
+	expect_channel_ready_event!(node_a_pj_receiver, node_b_pj_sender.node_id());
+	expect_channel_ready_event!(node_b_pj_sender, node_a_pj_receiver.node_id());
+	let _ = expect_payjoin_tx_sent_successfully_event!(node_b_pj_sender);
+	let channels = node_a_pj_receiver.list_channels();
+	let channel = channels.get(0).unwrap();
+	assert_eq!(channel.channel_value_sats, 80_000);
+	assert!(channel.is_channel_ready);
+	assert!(channel.is_usable);
+
+	assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().is_connected, true);
+	assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().is_persisted, true);
+	assert_eq!(node_a_pj_receiver.list_peers().get(0).unwrap().node_id, node_b_pj_sender.node_id());
+
+	let invoice_amount_1_msat = 2500_000;
+	let invoice =
+		node_b_pj_sender.bolt11_payment().receive(invoice_amount_1_msat, "test", 1000).unwrap();
+	assert!(node_a_pj_receiver.bolt11_payment().send(&invoice).is_ok());
+}