Skip to main content

r402_http/server/
layer.rs

1//! Axum middleware for enforcing [x402](https://www.x402.org) payments on protected routes.
2//!
3//! This middleware validates incoming payment headers using a configured x402 facilitator,
4//! and settles valid payments either before or after request execution (configurable).
5//!
6//! Returns a `402 Payment Required` response if the request lacks a valid payment.
7//!
8//! ## Settlement Timing
9//!
10//! By default, settlement occurs **after** the request is processed. You can change this behavior:
11//!
12//! - **[`X402Middleware::settle_before_execution`]** - Settle payment **before** request execution.
13//! - **[`X402Middleware::settle_after_execution`]** - Settle payment **after** request execution (default).
14//!   This allows processing the request before committing the payment on-chain.
15//!
16//! ## Configuration Notes
17//!
18//! - **[`X402Middleware::with_price_tag`]** sets the assets and amounts accepted for payment (static pricing).
19//! - **[`X402Middleware::with_dynamic_price`]** sets a callback for dynamic pricing based on request context.
20//! - **[`X402Middleware::with_base_url`]** sets the base URL for computing full resource URLs.
21//!   If not set, defaults to `http://localhost/` (avoid in production).
22//! - **[`X402LayerBuilder::with_description`]** is optional but helps the payer understand what is being paid for.
23//! - **[`X402LayerBuilder::with_mime_type`]** sets the MIME type of the protected resource (default: `application/json`).
24//! - **[`X402LayerBuilder::with_resource`]** explicitly sets the full URI of the protected resource.
25//!
26
27use std::convert::Infallible;
28use std::future::Future;
29use std::pin::Pin;
30use std::sync::Arc;
31use std::task::{Context, Poll};
32use std::time::Duration;
33
34use axum_core::extract::Request;
35use axum_core::response::Response;
36use http::{HeaderMap, Uri};
37use r402::facilitator::Facilitator;
38use r402::proto::v2;
39use tower::util::BoxCloneSyncService;
40use tower::{Layer, Service};
41use url::Url;
42
43use super::facilitator::FacilitatorClient;
44use super::paygate::{Paygate, ResourceInfoBuilder};
45use super::pricing::{DynamicPriceTags, PriceTagSource, StaticPriceTags};
46
47/// The main X402 middleware instance for enforcing x402 payments on routes.
48///
49/// Create a single instance per application and use it to build payment layers
50/// for protected routes.
51pub struct X402Middleware<F> {
52    facilitator: F,
53    base_url: Option<Url>,
54    settle_before_execution: bool,
55}
56
57impl<F: Clone> Clone for X402Middleware<F> {
58    fn clone(&self) -> Self {
59        Self {
60            facilitator: self.facilitator.clone(),
61            base_url: self.base_url.clone(),
62            settle_before_execution: self.settle_before_execution,
63        }
64    }
65}
66
67impl<F: std::fmt::Debug> std::fmt::Debug for X402Middleware<F> {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.debug_struct("X402Middleware")
70            .field("facilitator", &self.facilitator)
71            .field("base_url", &self.base_url)
72            .field("settle_before_execution", &self.settle_before_execution)
73            .finish()
74    }
75}
76
77impl<F> X402Middleware<F> {
78    /// Returns a reference to the underlying facilitator.
79    pub const fn facilitator(&self) -> &F {
80        &self.facilitator
81    }
82}
83
84impl X402Middleware<Arc<FacilitatorClient>> {
85    /// Creates a new middleware instance with a default facilitator URL.
86    ///
87    /// # Panics
88    ///
89    /// Panics if the facilitator URL is invalid.
90    #[must_use]
91    pub fn new(url: &str) -> Self {
92        let facilitator = FacilitatorClient::try_from(url).expect("Invalid facilitator URL");
93        Self {
94            facilitator: Arc::new(facilitator),
95            base_url: None,
96            settle_before_execution: false,
97        }
98    }
99
100    /// Creates a new middleware instance with a facilitator URL.
101    ///
102    /// # Errors
103    ///
104    /// Returns an error if the URL is invalid.
105    pub fn try_new(url: &str) -> Result<Self, Box<dyn std::error::Error>> {
106        let facilitator = FacilitatorClient::try_from(url)?;
107        Ok(Self {
108            facilitator: Arc::new(facilitator),
109            base_url: None,
110            settle_before_execution: false,
111        })
112    }
113
114    /// Returns the configured facilitator URL.
115    #[must_use]
116    pub fn facilitator_url(&self) -> &Url {
117        self.facilitator.base_url()
118    }
119
120    /// Sets the TTL for caching the facilitator's supported response.
121    ///
122    /// Default is 10 minutes. Use [`FacilitatorClient::without_supported_cache()`]
123    /// to disable caching entirely.
124    #[must_use]
125    pub fn with_supported_cache_ttl(&self, ttl: Duration) -> Self {
126        let inner = Arc::unwrap_or_clone(Arc::clone(&self.facilitator));
127        let facilitator = Arc::new(inner.with_supported_cache_ttl(ttl));
128        Self {
129            facilitator,
130            base_url: self.base_url.clone(),
131            settle_before_execution: self.settle_before_execution,
132        }
133    }
134}
135
136impl TryFrom<&str> for X402Middleware<Arc<FacilitatorClient>> {
137    type Error = Box<dyn std::error::Error>;
138
139    fn try_from(value: &str) -> Result<Self, Self::Error> {
140        Self::try_new(value)
141    }
142}
143
144impl TryFrom<String> for X402Middleware<Arc<FacilitatorClient>> {
145    type Error = Box<dyn std::error::Error>;
146
147    fn try_from(value: String) -> Result<Self, Self::Error> {
148        Self::try_new(&value)
149    }
150}
151
152impl<F> X402Middleware<F>
153where
154    F: Clone,
155{
156    /// Sets the base URL used to construct resource URLs dynamically.
157    ///
158    /// If [`X402LayerBuilder::with_resource`] is not called, this base URL is combined with
159    /// each request's path/query to compute the resource. If not set, defaults to `http://localhost/`.
160    ///
161    /// In production, prefer calling `with_resource` or setting a precise `base_url`.
162    #[must_use]
163    pub fn with_base_url(&self, base_url: Url) -> Self {
164        let mut this = self.clone();
165        this.base_url = Some(base_url);
166        this
167    }
168
169    /// Enables settlement prior to request execution.
170    /// When disabled (default), settlement occurs after successful request execution.
171    #[must_use]
172    pub fn settle_before_execution(&self) -> Self {
173        let mut this = self.clone();
174        this.settle_before_execution = true;
175        this
176    }
177
178    /// Disables settlement prior to request execution (default behavior).
179    ///
180    /// When disabled, settlement occurs after successful request execution.
181    /// This is the default behavior and allows the application to process
182    /// the request before committing the payment on-chain.
183    #[must_use]
184    pub fn settle_after_execution(&self) -> Self {
185        let mut this = self.clone();
186        this.settle_before_execution = false;
187        this
188    }
189}
190
191impl<TFacilitator> X402Middleware<TFacilitator>
192where
193    TFacilitator: Clone,
194{
195    /// Sets the price tag for the protected route.
196    ///
197    /// Creates a layer builder that can be further configured with additional
198    /// price tags and resource information.
199    #[must_use]
200    pub fn with_price_tag(
201        &self,
202        price_tag: v2::PriceTag,
203    ) -> X402LayerBuilder<StaticPriceTags, TFacilitator> {
204        X402LayerBuilder {
205            facilitator: self.facilitator.clone(),
206            price_source: StaticPriceTags::new(vec![price_tag]),
207            base_url: self.base_url.clone().map(Arc::new),
208            resource: Arc::new(ResourceInfoBuilder::default()),
209            settle_before_execution: self.settle_before_execution,
210        }
211    }
212
213    /// Sets a dynamic price source for the protected route.
214    ///
215    /// The `callback` receives request headers, URI, and base URL, and returns
216    /// a vector of V2 price tags.
217    #[must_use]
218    pub fn with_dynamic_price<F, Fut>(
219        &self,
220        callback: F,
221    ) -> X402LayerBuilder<DynamicPriceTags, TFacilitator>
222    where
223        F: Fn(&HeaderMap, &Uri, Option<&Url>) -> Fut + Send + Sync + 'static,
224        Fut: Future<Output = Vec<v2::PriceTag>> + Send + 'static,
225    {
226        X402LayerBuilder {
227            facilitator: self.facilitator.clone(),
228            price_source: DynamicPriceTags::new(callback),
229            base_url: self.base_url.clone().map(Arc::new),
230            resource: Arc::new(ResourceInfoBuilder::default()),
231            settle_before_execution: self.settle_before_execution,
232        }
233    }
234}
235
236/// Builder for configuring the X402 middleware layer.
237///
238/// Generic over `TSource` which implements [`PriceTagSource`] to support
239/// both static and dynamic pricing strategies.
240#[derive(Clone)]
241#[allow(missing_debug_implementations)] // generic types may not implement Debug
242pub struct X402LayerBuilder<TSource, TFacilitator> {
243    facilitator: TFacilitator,
244    settle_before_execution: bool,
245    base_url: Option<Arc<Url>>,
246    price_source: TSource,
247    resource: Arc<ResourceInfoBuilder>,
248}
249
250impl<TFacilitator> X402LayerBuilder<StaticPriceTags, TFacilitator> {
251    /// Adds another payment option.
252    ///
253    /// Allows specifying multiple accepted payment methods (e.g., different networks).
254    ///
255    /// Note: This method is only available for static price tag sources.
256    #[must_use]
257    pub fn with_price_tag(mut self, price_tag: v2::PriceTag) -> Self {
258        self.price_source = self.price_source.with_price_tag(price_tag);
259        self
260    }
261}
262
263#[allow(missing_debug_implementations)] // generic types may not implement Debug
264impl<TSource, TFacilitator> X402LayerBuilder<TSource, TFacilitator> {
265    /// Sets a description of what the payment grants access to.
266    ///
267    /// This is included in 402 responses to inform clients what they're paying for.
268    #[must_use]
269    pub fn with_description(mut self, description: String) -> Self {
270        let mut new_resource = (*self.resource).clone();
271        new_resource.description = description;
272        self.resource = Arc::new(new_resource);
273        self
274    }
275
276    /// Sets the MIME type of the protected resource.
277    ///
278    /// Defaults to `application/json` if not specified.
279    #[must_use]
280    pub fn with_mime_type(mut self, mime: String) -> Self {
281        let mut new_resource = (*self.resource).clone();
282        new_resource.mime_type = mime;
283        self.resource = Arc::new(new_resource);
284        self
285    }
286
287    /// Sets the full URL of the protected resource.
288    ///
289    /// When set, this URL is used directly instead of constructing it from the base URL
290    /// and request URI. This is the preferred approach in production.
291    #[must_use]
292    #[allow(clippy::needless_pass_by_value)] // Url consumed via to_string()
293    pub fn with_resource(mut self, resource: Url) -> Self {
294        let mut new_resource = (*self.resource).clone();
295        new_resource.url = Some(resource.to_string());
296        self.resource = Arc::new(new_resource);
297        self
298    }
299}
300
301impl<S, TSource, TFacilitator> Layer<S> for X402LayerBuilder<TSource, TFacilitator>
302where
303    S: Service<Request, Response = Response, Error = Infallible> + Clone + Send + Sync + 'static,
304    S::Future: Send + 'static,
305    TFacilitator: Facilitator + Clone,
306    TSource: PriceTagSource,
307{
308    type Service = X402MiddlewareService<TSource, TFacilitator>;
309
310    fn layer(&self, inner: S) -> Self::Service {
311        X402MiddlewareService {
312            facilitator: self.facilitator.clone(),
313            settle_before_execution: self.settle_before_execution,
314            base_url: self.base_url.clone(),
315            price_source: self.price_source.clone(),
316            resource: Arc::clone(&self.resource),
317            inner: BoxCloneSyncService::new(inner),
318        }
319    }
320}
321
322/// Axum service that enforces x402 payments on incoming requests.
323///
324/// Generic over `TSource` which implements [`PriceTagSource`] to support
325/// both static and dynamic pricing strategies.
326#[derive(Clone)]
327#[allow(missing_debug_implementations)] // BoxCloneSyncService does not implement Debug
328pub struct X402MiddlewareService<TSource, TFacilitator> {
329    /// Payment facilitator (local or remote)
330    facilitator: TFacilitator,
331    /// Base URL for constructing resource URLs
332    base_url: Option<Arc<Url>>,
333    /// Whether to settle payment before executing the request (true) or after (false)
334    settle_before_execution: bool,
335    /// Price tag source - can be static or dynamic
336    price_source: TSource,
337    /// Resource information
338    resource: Arc<ResourceInfoBuilder>,
339    /// The inner Axum service being wrapped
340    inner: BoxCloneSyncService<Request, Response, Infallible>,
341}
342
343impl<TSource, TFacilitator> Service<Request> for X402MiddlewareService<TSource, TFacilitator>
344where
345    TSource: PriceTagSource,
346    TFacilitator: Facilitator + Clone + Send + Sync + 'static,
347{
348    type Response = Response;
349    type Error = Infallible;
350    type Future = Pin<Box<dyn Future<Output = Result<Response, Infallible>> + Send>>;
351
352    /// Delegates readiness polling to the wrapped inner service.
353    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
354        self.inner.poll_ready(cx)
355    }
356
357    /// Intercepts the request, injects payment enforcement logic, and forwards to the wrapped service.
358    fn call(&mut self, req: Request) -> Self::Future {
359        let price_source = self.price_source.clone();
360        let facilitator = self.facilitator.clone();
361        let base_url = self.base_url.clone();
362        let resource_builder = Arc::clone(&self.resource);
363        let settle_before_execution = self.settle_before_execution;
364        let mut inner = self.inner.clone();
365
366        Box::pin(async move {
367            // Resolve price tags from the source
368            let accepts = price_source
369                .resolve(req.headers(), req.uri(), base_url.as_deref())
370                .await;
371
372            // If no price tags are configured, bypass payment enforcement
373            if accepts.is_empty() {
374                return inner.call(req).await;
375            }
376
377            let resource = resource_builder.as_resource_info(base_url.as_deref(), &req);
378
379            let gate = {
380                let mut gate = Paygate::builder(facilitator)
381                    .accepts(accepts)
382                    .resource(resource)
383                    .settle_before_execution(settle_before_execution)
384                    .build();
385                gate.enrich_accepts().await;
386                gate
387            };
388            gate.handle_request(inner, req).await
389        })
390    }
391}