From b96a9e1e5c87cc4436a902439501a51ff048a328 Mon Sep 17 00:00:00 2001 From: pysel Date: Fri, 21 Nov 2025 13:53:21 -0800 Subject: [PATCH 1/6] introduce dynamic server-side pricing --- Cargo.toml | 2 +- crates/x402-axum/Cargo.toml | 2 +- crates/x402-axum/src/layer.rs | 347 ++++++++++++---------------------- 3 files changed, 120 insertions(+), 231 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 437f3932..b42ef7fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "x402-rs" -version = "0.10.0" +version = "0.9.2" authors = ["Sergey Ukustov "] edition = "2024" license = "Apache-2.0" diff --git a/crates/x402-axum/Cargo.toml b/crates/x402-axum/Cargo.toml index 8bf44fe7..cb568624 100644 --- a/crates/x402-axum/Cargo.toml +++ b/crates/x402-axum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "x402-axum" -version = "0.6.2" +version = "0.6.1" edition = "2024" description = "Axum middleware for enforcing x402 protocol payments on protected routes" license = "Apache-2.0" diff --git a/crates/x402-axum/src/layer.rs b/crates/x402-axum/src/layer.rs index 62293fd9..04b8a6b1 100644 --- a/crates/x402-axum/src/layer.rs +++ b/crates/x402-axum/src/layer.rs @@ -69,7 +69,8 @@ use once_cell::sync::Lazy; use serde_json::json; use std::collections::HashSet; use std::fmt::{Debug, Display}; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; +use std::time::{SystemTime, UNIX_EPOCH}; use std::{ convert::Infallible, future::Future, @@ -126,7 +127,13 @@ pub struct X402Middleware { /// - a fully constructed list of [`PaymentRequirements`] (if [`X402Middleware::with_resource`] was used), /// - or a partial list without `resource`, in which case the resource URL will be computed dynamically per request. /// In this case, please add `base_url` via [`X402Middleware::with_base_url`]. - payment_offers: Arc, + payment_offers: Arc>, + /// Cached stale payment offers shared across all service instances. + /// + /// When prices are updated via [`X402Middleware::update_price_tags`], the current offers + /// are moved here and remain valid for a short grace period (defined by `STALE_PAYMENT_VALIDITY_MILLIS`). + /// This allows in-flight requests with old payment authorizations to complete successfully. + stale_payment_offers: Arc>>, } impl TryFrom<&str> for X402Middleware { @@ -160,7 +167,8 @@ impl X402Middleware { input_schema: None, output_schema: None, settle_before_execution: false, - payment_offers: Arc::new(PaymentOffers::Ready(Arc::new(Vec::new()))), + payment_offers: Arc::new(RwLock::new(PaymentOffers::Ready(Arc::new(Vec::new())))), + stale_payment_offers: Arc::new(RwLock::new(None)), } } @@ -345,80 +353,55 @@ where this } - /// Enables dynamic, per-request price computation via a callback. - /// - /// The callback receives request headers, URI, and base URL, and returns the - /// price amount for this request. The resource URL is automatically constructed - /// from the base URL and request URI, and all partial requirements are updated - /// with the dynamically computed price. + /// Updates the payment price tags dynamically, moving current offers to stale for graceful transition. /// - /// This is suitable for dynamic pricing flows where the exact amount depends on - /// request content, user context, or other runtime factors. + /// This method allows you to change pricing on the fly while ensuring that in-flight requests + /// with old payment authorizations can still complete successfully. The old prices remain + /// valid for a short grace period (defined by `STALE_PAYMENT_VALIDITY_MILLIS`, currently 5 seconds). /// /// # Example /// - /// ```rust,ignore - /// use x402_axum::layer::DynamicPriceCallback; + /// ```rust,no_run + /// use x402_axum::X402Middleware; + /// use x402_rs::network::{Network, USDCDeployment}; + /// use x402_axum::IntoPriceTag; /// - /// let callback: Box = Box::new(move |headers, uri, base_url| { - /// Box::pin(async move { - /// // Compute price based on request - /// Ok(TokenAmount::from(1000000)) - /// }) - /// }); + /// let x402 = X402Middleware::try_from("https://facilitator.example.com/") + /// .unwrap() + /// .with_price_tag( + /// USDCDeployment::by_network(Network::BaseSepolia) + /// .amount("0.01") + /// .pay_to("0xADDRESS") + /// .unwrap() + /// ); /// - /// x402.with_dynamic_price(callback); + /// // Later, update the price + /// x402.update_price_tags( + /// USDCDeployment::by_network(Network::BaseSepolia) + /// .amount("0.02") + /// .pay_to("0xADDRESS") + /// .unwrap() + /// ); /// ``` #[allow(dead_code)] // Public for consumption by downstream crates. - pub fn with_dynamic_price

(&self, price_callback: P) -> Self - where - P: for<'a> Fn( - &'a HeaderMap, - &'a Uri, - &'a Url, - ) - -> Pin> + Send + 'a>> - + Send - + Sync - + 'static, - { + pub fn update_price_tags>>(&self, new_price_tags: T) { + let new_price_tags = new_price_tags.into().clone(); + + // Move current offers to stale + let current_offers = self.payment_offers.read().unwrap().clone(); + let mut stale = self.stale_payment_offers.write().unwrap(); + *stale = Some(StalePaymentOffers::new(current_offers)); + drop(stale); + + // Create a new instance with the new price tags and recompute offers let mut this = self.clone(); - let base_url = this.base_url(); - let description = this.description.clone().unwrap_or_default(); - let mime_type = this - .mime_type - .clone() - .unwrap_or("application/json".to_string()); - let max_timeout_seconds = this.max_timeout_seconds; - let partial = this - .price_tag - .iter() - .map(|price_tag| { - let extra = if let Some(eip712) = price_tag.token.eip712.clone() { - Some(json!({ "name": eip712.name, "version": eip712.version })) - } else { - None - }; - PaymentRequirementsNoResource { - scheme: Scheme::Exact, - network: price_tag.token.network(), - max_amount_required: price_tag.amount, - description: description.clone(), - mime_type: mime_type.clone(), - pay_to: price_tag.pay_to.clone(), - max_timeout_seconds, - asset: price_tag.token.address(), - extra, - output_schema: None, - } - }) - .collect::>(); - this.payment_offers = Arc::new(PaymentOffers::DynamicPrice { - partial, - base_url, - price_callback: DynamicPriceFn::new(price_callback), - }); - this + this.price_tag = new_price_tags; + this = this.recompute_offers(); + + // Update the shared payment_offers with the newly computed offers + let mut offers = self.payment_offers.write().unwrap(); + *offers = this.payment_offers.read().unwrap().clone(); + drop(offers); } fn recompute_offers(mut self) -> Self { @@ -506,7 +489,8 @@ where base_url, } }; - self.payment_offers = Arc::new(payment_offers); + + self.payment_offers = Arc::new(RwLock::new(payment_offers)); self } } @@ -523,7 +507,9 @@ pub struct X402MiddlewareService { /// Payment facilitator (local or remote) facilitator: Arc, /// Payment requirements either with static or dynamic resource URLs - payment_offers: Arc, + payment_offers: Arc>, + /// Cached stale payment offers shared across all service instances. + stale_payment_offers: Arc>>, /// Whether to settle payment before executing the request (true) or after (false) settle_before_execution: bool, /// The inner Axum service being wrapped @@ -549,6 +535,7 @@ where facilitator: self.facilitator.clone(), payment_offers: self.payment_offers.clone(), settle_before_execution: self.settle_before_execution, + stale_payment_offers: self.stale_payment_offers.clone(), inner: BoxCloneSyncService::new(inner), } } @@ -569,20 +556,32 @@ where /// Intercepts the request, injects payment enforcement logic, and forwards to the wrapped service. fn call(&mut self, req: Request) -> Self::Future { - let offers = self.payment_offers.clone(); - let facilitator = self.facilitator.clone(); + let payment_offers = self.payment_offers.read().unwrap(); + let payment_requirements = + gather_payment_requirements(&payment_offers, req.uri()); + drop(payment_offers); + + let mut all_valid_payment_requirements = payment_requirements.as_ref().clone(); + + // Check if there are stale offers that are still valid + let stale_offers_guard = self.stale_payment_offers.read().unwrap(); + if let Some(ref stale) = *stale_offers_guard { + if stale.still_valid() { + let stale_requirements = gather_payment_requirements(&stale.payment_offers, req.uri()); + all_valid_payment_requirements.extend(stale_requirements.as_ref().clone()); + } + } + drop(stale_offers_guard); + + println!("All valid payment requirements: {:?}", all_valid_payment_requirements); + + let gate = X402Paygate { + facilitator: self.facilitator.clone(), + payment_requirements: Arc::new(all_valid_payment_requirements), + settle_before_execution: self.settle_before_execution, + }; let inner = self.inner.clone(); - let settle_before_execution = self.settle_before_execution; - Box::pin(async move { - let payment_requirements = - gather_payment_requirements(offers.as_ref(), req.uri(), req.headers()).await; - let gate = X402Paygate { - facilitator, - payment_requirements, - settle_before_execution, - }; - gate.call(inner, req).await - }) + Box::pin(gate.call(inner, req)) } } @@ -763,9 +762,8 @@ where ) -> Result { let selected = self .find_matching_payment_requirements(&payment_payload) - .ok_or(X402Error::no_payment_matching( - self.payment_requirements.as_ref().clone(), - ))?; + .ok_or(X402Error::no_payment_matching(self.payment_requirements.as_ref().clone()))?; + let verify_request = VerifyRequest { x402_version: payment_payload.x402_version, payment_payload, @@ -1014,13 +1012,39 @@ pub enum PaymentOffers { partial: Vec, base_url: Url, }, - /// Dynamically computed price per request using a user-provided callback. - /// The resource URL is automatically constructed from base URL and request URI. - DynamicPrice { - partial: Vec, - base_url: Url, - price_callback: DynamicPriceFn, - }, +} + +const STALE_PAYMENT_VALIDITY_MILLIS: u128 = 5_000; // 5 seconds + +#[derive(Clone, Debug)] +pub struct StalePaymentOffers { + pub payment_offers: PaymentOffers, + pub valid_until: u128, // unix timestamp in milliseconds +} + +impl StalePaymentOffers { + pub fn new(payment_offers: PaymentOffers) -> Self { + let now = SystemTime::now(); + let timestamp_duration = now + .duration_since(UNIX_EPOCH) + .expect("Time went backwards"); + + let valid_until = timestamp_duration.as_millis() + STALE_PAYMENT_VALIDITY_MILLIS; + + Self { + payment_offers, + valid_until, + } + } + + fn still_valid(&self) -> bool { + let now = SystemTime::now(); + let timestamp_duration = now + .duration_since(UNIX_EPOCH) + .expect("Time went backwards"); + + timestamp_duration.as_millis() < self.valid_until + } } /// Constructs a full list of [`PaymentRequirements`] for a request. @@ -1032,23 +1056,18 @@ pub enum PaymentOffers { /// - If `payment_offers` is [`PaymentOffers::NoResource`], it dynamically constructs the `resource` URI /// by combining the `base_url` with the request's path and query, and completes each /// partial `PaymentRequirementsNoResource` into a full `PaymentRequirements`. -/// - If `payment_offers` is [`PaymentOffers::DynamicPrice`], it constructs the resource URL, -/// calls the price callback to get the dynamic price, updates all partial requirements with -/// the new price, and converts them to full `PaymentRequirements`. /// /// # Arguments /// /// * `payment_offers` - The current payment offer configuration, either precomputed or partial. /// * `req_uri` - The incoming request URI used to construct the full resource path if needed. -/// * `req_headers` - The incoming request headers passed to the price callback if needed. /// /// # Returns /// /// An `Arc>` ready to be passed to a facilitator for verification. -async fn gather_payment_requirements( +fn gather_payment_requirements( payment_offers: &PaymentOffers, req_uri: &Uri, - req_headers: &HeaderMap, ) -> Arc> { match payment_offers { PaymentOffers::Ready(requirements) => { @@ -1068,135 +1087,5 @@ async fn gather_payment_requirements( .collect::>(); Arc::new(payment_requirements) } - PaymentOffers::DynamicPrice { - partial, - base_url, - price_callback, - } => { - // Build resource URL from base_url and request URI - let resource = { - let mut resource_url = base_url.clone(); - resource_url.set_path(req_uri.path()); - resource_url.set_query(req_uri.query()); - resource_url - }; - - // Call the price callback to get the dynamic price - match price_callback - .get_price(req_headers, req_uri, base_url) - .await - { - Ok(dynamic_price) => { - // Update all partial requirements with the dynamic price - let payment_requirements = partial - .iter() - .map(|partial| { - let mut req = partial.to_payment_requirements(resource.clone()); - req.max_amount_required = dynamic_price; - req - }) - .collect::>(); - Arc::new(payment_requirements) - } - Err(_) => { - // If price callback fails, fall back to NoResource behavior (use original prices) - let payment_requirements = partial - .iter() - .map(|partial| partial.to_payment_requirements(resource.clone())) - .collect::>(); - Arc::new(payment_requirements) - } - } - } - } -} - -/// Type alias for a dynamic price callback function signature. -/// -/// This callback receives request headers, URI, and base URL, and returns -/// the price amount for the request. It's used with [`X402Middleware::with_dynamic_price`]. -/// -/// The callback signature is: -/// ```rust,ignore -/// for<'a> Fn( -/// &'a HeaderMap, -/// &'a Uri, -/// &'a Url, -/// ) -> Pin> + Send + 'a>> -/// ``` -/// -/// # Example -/// -/// ```rust,ignore -/// use x402_axum::layer::DynamicPriceCallback; -/// use x402_rs::types::TokenAmount; -/// -/// // Define your price calculation logic -/// async fn calculate_price( -/// headers: &HeaderMap, -/// uri: &Uri, -/// base_url: &Url, -/// ) -> Result { -/// // Extract price from headers, cache, or compute dynamically -/// Ok(TokenAmount::from(1000000)) -/// } -/// -/// // Use it with with_dynamic_price -/// let callback = move |headers: &HeaderMap, uri: &Uri, base_url: &Url| { -/// Box::pin(calculate_price(headers, uri, base_url)) -/// }; -/// -/// x402.with_dynamic_price(callback); -/// ``` -pub type DynamicPriceCallback = dyn for<'a> Fn( - &'a HeaderMap, - &'a Uri, - &'a Url, - ) -> Pin> + Send + 'a>> - + Send - + Sync; - -/// A clonable wrapper for an async price callback function that computes per-request prices. -#[derive(Clone)] -pub struct DynamicPriceFn(Arc); - -impl Debug for DynamicPriceFn { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "DynamicPriceFn()") - } -} - -impl PartialEq for DynamicPriceFn { - fn eq(&self, _other: &Self) -> bool { - // Function pointers can't be meaningfully compared for equality - false - } -} - -impl Eq for DynamicPriceFn {} - -impl DynamicPriceFn { - pub fn new

(price_callback: P) -> Self - where - P: for<'a> Fn( - &'a HeaderMap, - &'a Uri, - &'a Url, - ) - -> Pin> + Send + 'a>> - + Send - + Sync - + 'static, - { - DynamicPriceFn(Arc::new(price_callback)) - } - - pub async fn get_price( - &self, - headers: &HeaderMap, - uri: &Uri, - base_url: &Url, - ) -> Result { - (self.0)(headers, uri, base_url).await } } From c0e60b64f700d1d845df4fb3bd91c09869441d2d Mon Sep 17 00:00:00 2001 From: pysel Date: Mon, 24 Nov 2025 14:06:43 -0800 Subject: [PATCH 2/6] bring back the updates from client side pricing --- crates/x402-axum/src/layer.rs | 232 +++++++++++++++++++++++++++++++++- 1 file changed, 226 insertions(+), 6 deletions(-) diff --git a/crates/x402-axum/src/layer.rs b/crates/x402-axum/src/layer.rs index 04b8a6b1..dc7dfe73 100644 --- a/crates/x402-axum/src/layer.rs +++ b/crates/x402-axum/src/layer.rs @@ -351,8 +351,84 @@ where let mut this = self.clone(); this.settle_before_execution = false; this - } + } + /// Enables dynamic, per-request price computation via a callback. + /// + /// The callback receives request headers, URI, and base URL, and returns the + /// price amount for this request. The resource URL is automatically constructed + /// from the base URL and request URI, and all partial requirements are updated + /// with the dynamically computed price. + /// + /// This is suitable for dynamic pricing flows where the exact amount depends on + /// request content, user context, or other runtime factors. + /// + /// # Example + /// + /// ```rust,ignore + /// use x402_axum::layer::DynamicPriceCallback; + /// + /// let callback: Box = Box::new(move |headers, uri, base_url| { + /// Box::pin(async move { + /// // Compute price based on request + /// Ok(TokenAmount::from(1000000)) + /// }) + /// }); + /// + /// x402.with_dynamic_price(callback); + /// ``` + #[allow(dead_code)] // Public for consumption by downstream crates. + pub fn with_dynamic_price

(&self, price_callback: P) -> Self + where + P: for<'a> Fn( + &'a HeaderMap, + &'a Uri, + &'a Url, + ) + -> Pin> + Send + 'a>> + + Send + + Sync + + 'static, + { + let mut this = self.clone(); + let base_url = this.base_url(); + let description = this.description.clone().unwrap_or_default(); + let mime_type = this + .mime_type + .clone() + .unwrap_or("application/json".to_string()); + let max_timeout_seconds = this.max_timeout_seconds; + let partial = this + .price_tag + .iter() + .map(|price_tag| { + let extra = if let Some(eip712) = price_tag.token.eip712.clone() { + Some(json!({ "name": eip712.name, "version": eip712.version })) + } else { + None + }; + PaymentRequirementsNoResource { + scheme: Scheme::Exact, + network: price_tag.token.network(), + max_amount_required: price_tag.amount, + description: description.clone(), + mime_type: mime_type.clone(), + pay_to: price_tag.pay_to.clone(), + max_timeout_seconds, + asset: price_tag.token.address(), + extra, + output_schema: None, + } + }) + .collect::>(); + this.payment_offers = Arc::new(PaymentOffers::DynamicPrice { + partial, + base_url, + price_callback: DynamicPriceFn::new(price_callback), + }); + this + } + /// Updates the payment price tags dynamically, moving current offers to stale for graceful transition. /// /// This method allows you to change pricing on the fly while ensuring that in-flight requests @@ -555,10 +631,10 @@ where } /// Intercepts the request, injects payment enforcement logic, and forwards to the wrapped service. - fn call(&mut self, req: Request) -> Self::Future { + async fn call(&mut self, req: Request) -> Self::Future { let payment_offers = self.payment_offers.read().unwrap(); let payment_requirements = - gather_payment_requirements(&payment_offers, req.uri()); + gather_payment_requirements(&payment_offers, req.uri(), req.headers()); drop(payment_offers); let mut all_valid_payment_requirements = payment_requirements.as_ref().clone(); @@ -567,7 +643,7 @@ where let stale_offers_guard = self.stale_payment_offers.read().unwrap(); if let Some(ref stale) = *stale_offers_guard { if stale.still_valid() { - let stale_requirements = gather_payment_requirements(&stale.payment_offers, req.uri()); + let stale_requirements = gather_payment_requirements(&stale.payment_offers, req.uri(), req.headers()).await; all_valid_payment_requirements.extend(stale_requirements.as_ref().clone()); } } @@ -762,7 +838,9 @@ where ) -> Result { let selected = self .find_matching_payment_requirements(&payment_payload) - .ok_or(X402Error::no_payment_matching(self.payment_requirements.as_ref().clone()))?; + .ok_or(X402Error::no_payment_matching( + self.payment_requirements.as_ref().clone() + ))?; let verify_request = VerifyRequest { x402_version: payment_payload.x402_version, @@ -1012,6 +1090,13 @@ pub enum PaymentOffers { partial: Vec, base_url: Url, }, + /// Dynamically computed price per request using a user-provided callback. + /// The resource URL is automatically constructed from base URL and request URI. + DynamicPrice { + partial: Vec, + base_url: Url, + price_callback: DynamicPriceFn, + }, } const STALE_PAYMENT_VALIDITY_MILLIS: u128 = 5_000; // 5 seconds @@ -1056,18 +1141,23 @@ impl StalePaymentOffers { /// - If `payment_offers` is [`PaymentOffers::NoResource`], it dynamically constructs the `resource` URI /// by combining the `base_url` with the request's path and query, and completes each /// partial `PaymentRequirementsNoResource` into a full `PaymentRequirements`. +/// - If `payment_offers` is [`PaymentOffers::DynamicPrice`], it constructs the resource URL, +/// calls the price callback to get the dynamic price, updates all partial requirements with +/// the new price, and converts them to full `PaymentRequirements`. /// /// # Arguments /// /// * `payment_offers` - The current payment offer configuration, either precomputed or partial. /// * `req_uri` - The incoming request URI used to construct the full resource path if needed. +/// * `req_headers` - The incoming request headers passed to the price callback if needed. /// /// # Returns /// /// An `Arc>` ready to be passed to a facilitator for verification. -fn gather_payment_requirements( +async fn gather_payment_requirements( payment_offers: &PaymentOffers, req_uri: &Uri, + req_headers: &HeaderMap, ) -> Arc> { match payment_offers { PaymentOffers::Ready(requirements) => { @@ -1087,5 +1177,135 @@ fn gather_payment_requirements( .collect::>(); Arc::new(payment_requirements) } + PaymentOffers::DynamicPrice { + partial, + base_url, + price_callback, + } => { + // Build resource URL from base_url and request URI + let resource = { + let mut resource_url = base_url.clone(); + resource_url.set_path(req_uri.path()); + resource_url.set_query(req_uri.query()); + resource_url + }; + + // Call the price callback to get the dynamic price + match price_callback + .get_price(req_headers, req_uri, base_url) + .await + { + Ok(dynamic_price) => { + // Update all partial requirements with the dynamic price + let payment_requirements = partial + .iter() + .map(|partial| { + let mut req = partial.to_payment_requirements(resource.clone()); + req.max_amount_required = dynamic_price; + req + }) + .collect::>(); + Arc::new(payment_requirements) + } + Err(_) => { + // If price callback fails, fall back to NoResource behavior (use original prices) + let payment_requirements = partial + .iter() + .map(|partial| partial.to_payment_requirements(resource.clone())) + .collect::>(); + Arc::new(payment_requirements) + } + } + } } } + +/// Type alias for a dynamic price callback function signature. +/// +/// This callback receives request headers, URI, and base URL, and returns +/// the price amount for the request. It's used with [`X402Middleware::with_dynamic_price`]. +/// +/// The callback signature is: +/// ```rust,ignore +/// for<'a> Fn( +/// &'a HeaderMap, +/// &'a Uri, +/// &'a Url, +/// ) -> Pin> + Send + 'a>> +/// ``` +/// +/// # Example +/// +/// ```rust,ignore +/// use x402_axum::layer::DynamicPriceCallback; +/// use x402_rs::types::TokenAmount; +/// +/// // Define your price calculation logic +/// async fn calculate_price( +/// headers: &HeaderMap, +/// uri: &Uri, +/// base_url: &Url, +/// ) -> Result { +/// // Extract price from headers, cache, or compute dynamically +/// Ok(TokenAmount::from(1000000)) +/// } +/// +/// // Use it with with_dynamic_price +/// let callback = move |headers: &HeaderMap, uri: &Uri, base_url: &Url| { +/// Box::pin(calculate_price(headers, uri, base_url)) +/// }; +/// +/// x402.with_dynamic_price(callback); +/// ``` +pub type DynamicPriceCallback = dyn for<'a> Fn( + &'a HeaderMap, + &'a Uri, + &'a Url, +) -> Pin> + Send + 'a>> ++ Send ++ Sync; + +/// A clonable wrapper for an async price callback function that computes per-request prices. +#[derive(Clone)] +pub struct DynamicPriceFn(Arc); + +impl Debug for DynamicPriceFn { +fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "DynamicPriceFn()") +} +} + +impl PartialEq for DynamicPriceFn { +fn eq(&self, _other: &Self) -> bool { + // Function pointers can't be meaningfully compared for equality + false +} +} + +impl Eq for DynamicPriceFn {} + +impl DynamicPriceFn { +pub fn new

(price_callback: P) -> Self +where + P: for<'a> Fn( + &'a HeaderMap, + &'a Uri, + &'a Url, + ) + -> Pin> + Send + 'a>> + + Send + + Sync + + 'static, +{ + DynamicPriceFn(Arc::new(price_callback)) +} + +pub async fn get_price( + &self, + headers: &HeaderMap, + uri: &Uri, + base_url: &Url, +) -> Result { + (self.0)(headers, uri, base_url).await + } +} \ No newline at end of file From 5eb2b07671e7dbaa1369425bd6ebf260cdea098c Mon Sep 17 00:00:00 2001 From: pysel Date: Mon, 24 Nov 2025 14:09:39 -0800 Subject: [PATCH 3/6] formatting --- crates/x402-axum/src/layer.rs | 58 +++++++++++++++++------------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/crates/x402-axum/src/layer.rs b/crates/x402-axum/src/layer.rs index dc7dfe73..04f56cde 100644 --- a/crates/x402-axum/src/layer.rs +++ b/crates/x402-axum/src/layer.rs @@ -1270,42 +1270,42 @@ pub type DynamicPriceCallback = dyn for<'a> Fn( pub struct DynamicPriceFn(Arc); impl Debug for DynamicPriceFn { -fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "DynamicPriceFn()") -} + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "DynamicPriceFn()") + } } impl PartialEq for DynamicPriceFn { -fn eq(&self, _other: &Self) -> bool { - // Function pointers can't be meaningfully compared for equality - false -} -} + fn eq(&self, _other: &Self) -> bool { + // Function pointers can't be meaningfully compared for equality + false + } + } impl Eq for DynamicPriceFn {} impl DynamicPriceFn { -pub fn new

(price_callback: P) -> Self -where - P: for<'a> Fn( - &'a HeaderMap, - &'a Uri, - &'a Url, - ) - -> Pin> + Send + 'a>> - + Send - + Sync - + 'static, -{ - DynamicPriceFn(Arc::new(price_callback)) -} + pub fn new

(price_callback: P) -> Self + where + P: for<'a> Fn( + &'a HeaderMap, + &'a Uri, + &'a Url, + ) + -> Pin> + Send + 'a>> + + Send + + Sync + + 'static, + { + DynamicPriceFn(Arc::new(price_callback)) + } -pub async fn get_price( - &self, - headers: &HeaderMap, - uri: &Uri, - base_url: &Url, -) -> Result { - (self.0)(headers, uri, base_url).await + pub async fn get_price( + &self, + headers: &HeaderMap, + uri: &Uri, + base_url: &Url, + ) -> Result { + (self.0)(headers, uri, base_url).await } } \ No newline at end of file From aa6dc8fbec677434b2777d6535a2fd49c9e45fc1 Mon Sep 17 00:00:00 2001 From: pysel Date: Mon, 24 Nov 2025 14:10:44 -0800 Subject: [PATCH 4/6] more formatting --- crates/x402-axum/src/layer.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/x402-axum/src/layer.rs b/crates/x402-axum/src/layer.rs index 04f56cde..6b402f85 100644 --- a/crates/x402-axum/src/layer.rs +++ b/crates/x402-axum/src/layer.rs @@ -1258,12 +1258,12 @@ async fn gather_payment_requirements( /// x402.with_dynamic_price(callback); /// ``` pub type DynamicPriceCallback = dyn for<'a> Fn( - &'a HeaderMap, - &'a Uri, - &'a Url, -) -> Pin> + Send + 'a>> -+ Send -+ Sync; + &'a HeaderMap, + &'a Uri, + &'a Url, + ) -> Pin> + Send + 'a>> + + Send + + Sync; /// A clonable wrapper for an async price callback function that computes per-request prices. #[derive(Clone)] @@ -1280,7 +1280,7 @@ impl PartialEq for DynamicPriceFn { // Function pointers can't be meaningfully compared for equality false } - } +} impl Eq for DynamicPriceFn {} @@ -1308,4 +1308,4 @@ impl DynamicPriceFn { ) -> Result { (self.0)(headers, uri, base_url).await } -} \ No newline at end of file +} From 4d458488a5fae7db20a7328f9490b3f77867f711 Mon Sep 17 00:00:00 2001 From: pysel Date: Mon, 24 Nov 2025 15:35:02 -0800 Subject: [PATCH 5/6] add todo --- crates/x402-axum/src/layer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/x402-axum/src/layer.rs b/crates/x402-axum/src/layer.rs index 6b402f85..5eda8417 100644 --- a/crates/x402-axum/src/layer.rs +++ b/crates/x402-axum/src/layer.rs @@ -1099,7 +1099,7 @@ pub enum PaymentOffers { }, } -const STALE_PAYMENT_VALIDITY_MILLIS: u128 = 5_000; // 5 seconds +const STALE_PAYMENT_VALIDITY_MILLIS: u128 = 5_000; // 5 seconds TODO: configurable #[derive(Clone, Debug)] pub struct StalePaymentOffers { From dede8f2fb49d5127c025914bbdd2f7ba41234e3f Mon Sep 17 00:00:00 2001 From: pysel Date: Mon, 24 Nov 2025 15:39:19 -0800 Subject: [PATCH 6/6] verify with commit --- crates/x402-axum/src/layer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/x402-axum/src/layer.rs b/crates/x402-axum/src/layer.rs index 5eda8417..1e98812e 100644 --- a/crates/x402-axum/src/layer.rs +++ b/crates/x402-axum/src/layer.rs @@ -1099,7 +1099,7 @@ pub enum PaymentOffers { }, } -const STALE_PAYMENT_VALIDITY_MILLIS: u128 = 5_000; // 5 seconds TODO: configurable +const STALE_PAYMENT_VALIDITY_MILLIS: u128 = 5_000; // 5 seconds TODO: configurable. #[derive(Clone, Debug)] pub struct StalePaymentOffers {