use crate::client::DiscoveryFilters;
use crate::types::{
DiscoveryResponse, FacilitatorConfig, PaymentPayload, PaymentRequirements, SettleResponse,
SupportedKinds, VerifyResponse,
};
use crate::{Result, X402Error};
use reqwest::Client;
pub mod coinbase;
#[cfg(test)]
mod tests;
pub const DEFAULT_FACILITATOR_URL: &str = "https://x402.org/facilitator";
#[derive(Clone)]
pub struct FacilitatorClient {
url: String,
client: Client,
auth_config: Option<crate::types::AuthHeadersFnArc>,
}
impl std::fmt::Debug for FacilitatorClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FacilitatorClient")
.field("url", &self.url)
.field("auth_config", &"<function>")
.finish()
}
}
impl FacilitatorClient {
pub fn new(config: FacilitatorConfig) -> Result<Self> {
config.validate()?;
let mut client_builder = Client::builder();
if let Some(timeout) = config.timeout {
client_builder = client_builder.timeout(timeout);
}
let client = client_builder
.build()
.map_err(|e| X402Error::config(format!("Failed to create HTTP client: {}", e)))?;
Ok(Self {
url: config.url,
client,
auth_config: config.create_auth_headers,
})
}
pub async fn verify(
&self,
payment_payload: &PaymentPayload,
payment_requirements: &PaymentRequirements,
) -> Result<VerifyResponse> {
tracing::debug!(
"Payment payload: {}",
serde_json::to_string_pretty(payment_payload).unwrap_or_default()
);
tracing::debug!(
"Payment requirements: {}",
serde_json::to_string_pretty(payment_requirements).unwrap_or_default()
);
let request_body = serde_json::json!({
"x402Version": crate::types::X402_VERSION,
"paymentPayload": serde_json::to_value(payment_payload).map_err(|e| X402Error::facilitator_error(format!("Failed to serialize payment payload: {}", e)))?,
"paymentRequirements": serde_json::to_value(payment_requirements).map_err(|e| X402Error::facilitator_error(format!("Failed to serialize payment requirements: {}", e)))?,
});
tracing::debug!(
"Facilitator verify request body: {}",
serde_json::to_string_pretty(&request_body).unwrap_or_default()
);
tracing::debug!("Sending request to: {}/verify", self.url);
let mut request = self
.client
.post(format!("{}/verify", self.url))
.json(&request_body);
if let Some(auth_config) = &self.auth_config {
let headers = auth_config()?;
if let Some(verify_headers) = headers.get("verify") {
for (key, value) in verify_headers {
request = request.header(key, value);
}
}
}
let response = request.send().await?;
let status = response.status();
if !status.is_success() {
let response_body = response
.text()
.await
.unwrap_or_else(|_| "Unable to read response body".to_string());
tracing::error!(
"Facilitator verify failed with status: {}. Request body: {}. Response body: {}",
status,
serde_json::to_string_pretty(&request_body).unwrap_or_default(),
response_body
);
return Err(X402Error::facilitator_error(format!(
"Verification failed with status: {}. Response: {}. Request: {}",
status,
response_body,
serde_json::to_string(&request_body).unwrap_or_default()
)));
}
let verify_response: VerifyResponse = response.json().await?;
Ok(verify_response)
}
pub async fn settle(
&self,
payment_payload: &PaymentPayload,
payment_requirements: &PaymentRequirements,
) -> Result<SettleResponse> {
let request_body = serde_json::json!({
"x402Version": crate::types::X402_VERSION,
"paymentPayload": serde_json::to_value(payment_payload).map_err(|e| X402Error::facilitator_error(format!("Failed to serialize payment payload: {}", e)))?,
"paymentRequirements": serde_json::to_value(payment_requirements).map_err(|e| X402Error::facilitator_error(format!("Failed to serialize payment requirements: {}", e)))?,
});
let mut request = self
.client
.post(format!("{}/settle", self.url))
.json(&request_body);
if let Some(auth_config) = &self.auth_config {
let headers = auth_config()?;
if let Some(settle_headers) = headers.get("settle") {
for (key, value) in settle_headers {
request = request.header(key, value);
}
}
}
let response = request.send().await?;
if !response.status().is_success() {
return Err(X402Error::facilitator_error(format!(
"Settlement failed with status: {}",
response.status()
)));
}
let settle_response: SettleResponse = response.json().await?;
Ok(settle_response)
}
pub async fn supported(&self) -> Result<SupportedKinds> {
let mut request = self.client.get(format!("{}/supported", self.url));
if let Some(auth_config) = &self.auth_config {
let headers = auth_config()?;
if let Some(supported_headers) = headers.get("supported") {
for (key, value) in supported_headers {
request = request.header(key, value);
}
}
}
let response = request.send().await?;
if !response.status().is_success() {
return Err(X402Error::facilitator_error(format!(
"Failed to get supported kinds with status: {}",
response.status()
)));
}
let supported: SupportedKinds = response.json().await?;
Ok(supported)
}
pub fn url(&self) -> &str {
&self.url
}
pub fn for_network(_network: &str, config: FacilitatorConfig) -> Result<Self> {
Self::new(config)
}
pub fn for_base_mainnet(config: FacilitatorConfig) -> Result<Self> {
Self::for_network("base", config)
}
pub fn for_base_sepolia(config: FacilitatorConfig) -> Result<Self> {
Self::for_network("base-sepolia", config)
}
pub async fn verify_with_network_validation(
&self,
payment_payload: &PaymentPayload,
payment_requirements: &PaymentRequirements,
) -> Result<VerifyResponse> {
if payment_payload.network != payment_requirements.network {
return Err(X402Error::payment_verification_failed(format!(
"CRITICAL ERROR: Network mismatch detected! Payment network '{}' does not match requirements network '{}'. This is a security violation.",
payment_payload.network, payment_requirements.network
)));
}
if payment_payload.scheme != payment_requirements.scheme {
return Err(X402Error::payment_verification_failed(format!(
"Scheme mismatch: payment scheme {} != requirements scheme {}",
payment_payload.scheme, payment_requirements.scheme
)));
}
self.verify(payment_payload, payment_requirements).await
}
pub async fn list(&self, filters: Option<DiscoveryFilters>) -> Result<DiscoveryResponse> {
let mut request = self.client.get(format!("{}/discovery/resources", self.url));
if let Some(filters) = filters {
if let Some(resource_type) = filters.resource_type {
request = request.query(&[("type", resource_type)]);
}
if let Some(limit) = filters.limit {
request = request.query(&[("limit", limit.to_string())]);
}
if let Some(offset) = filters.offset {
request = request.query(&[("offset", offset.to_string())]);
}
}
if let Some(auth_config) = &self.auth_config {
let headers = auth_config()?;
if let Some(discovery_headers) = headers.get("list") {
for (key, value) in discovery_headers {
request = request.header(key, value);
}
}
}
let response = request.send().await?;
if !response.status().is_success() {
return Err(X402Error::facilitator_error(format!(
"Discovery failed with status: {}",
response.status()
)));
}
let discovery_response: DiscoveryResponse = response.json().await?;
Ok(discovery_response)
}
pub async fn list_all(&self) -> Result<DiscoveryResponse> {
self.list(None).await
}
pub async fn list_by_type(&self, resource_type: &str) -> Result<DiscoveryResponse> {
let filters = DiscoveryFilters::new().with_resource_type(resource_type);
self.list(Some(filters)).await
}
}
impl Default for FacilitatorClient {
fn default() -> Self {
Self::new(FacilitatorConfig::default()).unwrap_or_else(|_| {
Self {
url: "https://x402.org/facilitator".to_string(),
client: Client::new(),
auth_config: None,
}
})
}
}