Skip to main content

r402_http/server/
paygate.rs

1//! Core payment gate logic for enforcing x402 payments (V2-only).
2//!
3//! The [`Paygate`] struct handles the full payment lifecycle:
4//! extracting headers, verifying with the facilitator, settling on-chain,
5//! and returning 402 responses when payment is required.
6
7use std::convert::Infallible;
8use std::sync::Arc;
9
10use axum_core::body::Body;
11use axum_core::extract::Request;
12use axum_core::response::{IntoResponse, Response};
13use http::{HeaderMap, HeaderValue, StatusCode};
14use r402::facilitator::Facilitator;
15use r402::proto;
16use r402::proto::Base64Bytes;
17use r402::proto::v2;
18use serde_json::json;
19use tower::Service;
20#[cfg(feature = "telemetry")]
21use tracing::{Instrument, instrument};
22use url::Url;
23
24use super::{PaygateError, VerificationError};
25
26/// Builder for resource information that can be used with both V1 and V2 protocols.
27#[derive(Debug, Clone)]
28pub struct ResourceInfoBuilder {
29    /// Description of the protected resource
30    pub description: String,
31    /// MIME type of the protected resource
32    pub mime_type: String,
33    /// Optional explicit URL of the protected resource
34    pub url: Option<String>,
35}
36
37impl Default for ResourceInfoBuilder {
38    fn default() -> Self {
39        Self {
40            description: String::new(),
41            mime_type: "application/json".to_string(),
42            url: None,
43        }
44    }
45}
46
47impl ResourceInfoBuilder {
48    /// Determines the resource URL (static or dynamic).
49    ///
50    /// If `url` is set, returns it directly. Otherwise, constructs a URL by combining
51    /// the base URL with the request URI's path and query.
52    ///
53    /// # Panics
54    ///
55    /// Panics if internal URL construction fails (should not happen in practice).
56    #[allow(clippy::unwrap_used)]
57    pub fn as_resource_info(&self, base_url: Option<&Url>, req: &Request) -> v2::ResourceInfo {
58        let url = self.url.clone().unwrap_or_else(|| {
59            let mut url = base_url.cloned().unwrap_or_else(|| {
60                let host = req.headers().get("host").and_then(|h| h.to_str().ok()).unwrap_or("localhost");
61                let origin = format!("http://{host}");
62                let url = Url::parse(&origin).unwrap_or_else(|_| Url::parse("http://localhost").unwrap());
63                #[cfg(feature = "telemetry")]
64                tracing::warn!(
65                    "X402Middleware base_url is not configured; using {url} as origin for resource resolution"
66                );
67                url
68            });
69            let request_uri = req.uri();
70            url.set_path(request_uri.path());
71            url.set_query(request_uri.query());
72            url.to_string()
73        });
74        v2::ResourceInfo {
75            description: self.description.clone(),
76            mime_type: self.mime_type.clone(),
77            url,
78        }
79    }
80}
81
82/// V2-only payment gate for enforcing x402 payments.
83///
84/// Handles the full payment lifecycle: header extraction, verification,
85/// settlement, and 402 response generation using the V2 wire format.
86///
87/// Construct via [`PaygateBuilder`] (obtained from [`Paygate::builder`]).
88///
89/// To add lifecycle hooks (before/after verify and settle), wrap your
90/// facilitator with [`HookedFacilitator`](r402::hooks::HookedFacilitator)
91/// before passing it to the payment gate.
92#[allow(missing_debug_implementations)]
93pub struct Paygate<TFacilitator> {
94    pub(crate) facilitator: TFacilitator,
95    pub(crate) accepts: Arc<Vec<v2::PriceTag>>,
96    pub(crate) resource: v2::ResourceInfo,
97}
98
99/// Builder for constructing a [`Paygate`] with validated configuration.
100///
101/// # Example
102///
103/// ```ignore
104/// let gate = Paygate::builder(facilitator)
105///     .accept(price_tag)
106///     .resource(resource_info)
107///     .build();
108/// ```
109#[allow(missing_debug_implementations)]
110pub struct PaygateBuilder<TFacilitator> {
111    facilitator: TFacilitator,
112    accepts: Vec<v2::PriceTag>,
113    resource: Option<v2::ResourceInfo>,
114}
115
116impl<TFacilitator> Paygate<TFacilitator> {
117    /// Returns a new builder seeded with the given facilitator.
118    pub const fn builder(facilitator: TFacilitator) -> PaygateBuilder<TFacilitator> {
119        PaygateBuilder {
120            facilitator,
121            accepts: Vec::new(),
122            resource: None,
123        }
124    }
125
126    /// Returns a reference to the underlying facilitator.
127    pub const fn facilitator(&self) -> &TFacilitator {
128        &self.facilitator
129    }
130
131    /// Returns a reference to the accepted price tags.
132    pub fn accepts(&self) -> &[v2::PriceTag] {
133        &self.accepts
134    }
135
136    /// Returns a reference to the resource information.
137    pub const fn resource(&self) -> &v2::ResourceInfo {
138        &self.resource
139    }
140
141    /// Converts a [`PaygateError`] into a proper 402 HTTP response using
142    /// this gate's accepted price tags and resource information.
143    ///
144    /// This is the public convenience wrapper around [`error_into_response`],
145    /// useful in composable flows where the caller handles verification
146    /// separately from settlement.
147    #[must_use]
148    pub fn error_response(&self, err: PaygateError) -> Response {
149        error_into_response(err, &self.accepts, &self.resource)
150    }
151}
152
153impl<TFacilitator> PaygateBuilder<TFacilitator> {
154    /// Adds a single accepted payment option.
155    #[must_use]
156    pub fn accept(mut self, price_tag: v2::PriceTag) -> Self {
157        self.accepts.push(price_tag);
158        self
159    }
160
161    /// Adds multiple accepted payment options.
162    #[must_use]
163    pub fn accepts(mut self, price_tags: impl IntoIterator<Item = v2::PriceTag>) -> Self {
164        self.accepts.extend(price_tags);
165        self
166    }
167
168    /// Sets the resource metadata returned in 402 responses.
169    #[must_use]
170    pub fn resource(mut self, resource: v2::ResourceInfo) -> Self {
171        self.resource = Some(resource);
172        self
173    }
174
175    /// Consumes the builder and produces a configured [`Paygate`].
176    ///
177    /// Uses empty resource info if none was provided.
178    pub fn build(self) -> Paygate<TFacilitator> {
179        Paygate {
180            facilitator: self.facilitator,
181            accepts: Arc::new(self.accepts),
182            resource: self.resource.unwrap_or_else(|| v2::ResourceInfo {
183                description: String::new(),
184                mime_type: "application/json".to_owned(),
185                url: String::new(),
186            }),
187        }
188    }
189}
190
191/// The V2 payment header name.
192pub const PAYMENT_HEADER_NAME: &str = "Payment-Signature";
193
194/// The V2 payment payload type.
195pub type V2PaymentPayload = v2::PaymentPayload<v2::PaymentRequirements, serde_json::Value>;
196
197/// A verified payment token ready for on-chain settlement.
198///
199/// Produced by [`Paygate::verify_only`] after the facilitator confirms the
200/// payment signature is valid. The caller controls *when* settlement happens:
201///
202/// - **Synchronous**: call [`settle`](Self::settle) immediately, then attach
203///   the header via [`settlement_to_header`].
204/// - **Deferred**: stream the response first, then settle and append the
205///   result (e.g., as an SSE trailer event).
206///
207/// [`settle`](Self::settle) **consumes** `self`, preventing double-settlement
208/// at the type level.
209#[derive(Debug)]
210pub struct VerifiedPayment {
211    /// The settle request derived from the verified payment payload.
212    settle_request: proto::SettleRequest,
213}
214
215impl VerifiedPayment {
216    /// Execute on-chain settlement for this verified payment.
217    ///
218    /// Takes ownership of `self` to prevent double-settlement.
219    ///
220    /// # Errors
221    ///
222    /// Returns [`PaygateError::Settlement`] if the facilitator rejects the
223    /// settlement or if the on-chain transaction fails.
224    pub async fn settle<F: Facilitator>(
225        self,
226        facilitator: &F,
227    ) -> Result<proto::SettleResponse, PaygateError> {
228        let settlement = facilitator
229            .settle(self.settle_request)
230            .await
231            .map_err(|e| PaygateError::Settlement(format!("{e}")))?;
232
233        if let proto::SettleResponse::Error {
234            reason, message, ..
235        } = &settlement
236        {
237            let detail = message.as_deref().unwrap_or(reason.as_str());
238            return Err(PaygateError::Settlement(detail.to_owned()));
239        }
240
241        Ok(settlement)
242    }
243
244    /// Returns a reference to the underlying settle request.
245    #[must_use]
246    pub const fn settle_request(&self) -> &proto::SettleRequest {
247        &self.settle_request
248    }
249}
250
251impl<TFacilitator> Paygate<TFacilitator> {
252    /// Calls the inner service with proper telemetry instrumentation.
253    async fn call_inner<
254        ReqBody,
255        ResBody,
256        S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
257    >(
258        mut inner: S,
259        req: http::Request<ReqBody>,
260    ) -> Result<http::Response<ResBody>, S::Error>
261    where
262        S::Future: Send,
263    {
264        #[cfg(feature = "telemetry")]
265        {
266            inner
267                .call(req)
268                .instrument(tracing::info_span!("inner"))
269                .await
270        }
271        #[cfg(not(feature = "telemetry"))]
272        {
273            inner.call(req).await
274        }
275    }
276}
277
278impl<TFacilitator> Paygate<TFacilitator>
279where
280    TFacilitator: Facilitator + Sync,
281{
282    /// Handles an incoming request, processing payment if required.
283    ///
284    /// Returns 402 response if payment fails.
285    /// Otherwise, returns the response from the inner service.
286    ///
287    /// # Errors
288    ///
289    /// This method is infallible (`Infallible` error type).
290    #[cfg_attr(
291        feature = "telemetry",
292        instrument(name = "x402.handle_request", skip_all)
293    )]
294    pub async fn handle_request<
295        ReqBody,
296        ResBody,
297        S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
298    >(
299        &self,
300        inner: S,
301        req: http::Request<ReqBody>,
302    ) -> Result<Response, Infallible>
303    where
304        S::Response: IntoResponse,
305        S::Error: IntoResponse,
306        S::Future: Send,
307    {
308        match self.handle_request_fallible(inner, req).await {
309            Ok(response) => Ok(response),
310            Err(err) => Ok(error_into_response(err, &self.accepts, &self.resource)),
311        }
312    }
313
314    /// Enriches price tags with facilitator capabilities (e.g., fee payer address).
315    pub async fn enrich_accepts(&mut self) {
316        let capabilities = self.facilitator.supported().await.unwrap_or_default();
317
318        let accepts = (*self.accepts)
319            .clone()
320            .into_iter()
321            .map(|mut pt| {
322                pt.enrich(&capabilities);
323                pt
324            })
325            .collect::<Vec<_>>();
326        self.accepts = Arc::new(accepts);
327    }
328
329    /// Verify the payment from request headers without executing the inner
330    /// service or settling on-chain.
331    ///
332    /// Returns a [`VerifiedPayment`] token on success, which the caller can
333    /// later [`settle`](VerifiedPayment::settle) at their discretion.
334    ///
335    /// This is the building block for **composable** payment flows such as
336    /// deferred settlement for streaming responses.
337    ///
338    /// # Errors
339    ///
340    /// Returns [`PaygateError::Verification`] if the payment header is missing,
341    /// malformed, or rejected by the facilitator.
342    #[cfg_attr(feature = "telemetry", instrument(name = "x402.verify_only", skip_all))]
343    pub async fn verify_only(&self, headers: &HeaderMap) -> Result<VerifiedPayment, PaygateError> {
344        let header = extract_payment_header(headers, PAYMENT_HEADER_NAME).ok_or(
345            VerificationError::PaymentHeaderRequired(PAYMENT_HEADER_NAME),
346        )?;
347        let payment_payload = extract_payment_payload::<V2PaymentPayload>(header)
348            .ok_or(VerificationError::InvalidPaymentHeader)?;
349
350        let verify_request = make_verify_request(payment_payload, &self.accepts)?;
351
352        let verify_response = self
353            .facilitator
354            .verify(verify_request.clone())
355            .await
356            .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
357
358        validate_verify_response(verify_response)?;
359
360        Ok(VerifiedPayment {
361            settle_request: verify_request.into(),
362        })
363    }
364
365    /// Handles an incoming request, returning errors as `PaygateError`.
366    ///
367    /// This is the fallible version of [`handle_request`](Self::handle_request)
368    /// that returns an actual error instead of converting it into a 402
369    /// Payment Required response.
370    ///
371    /// Internally delegates to [`verify_only`](Self::verify_only) for
372    /// verification and [`VerifiedPayment::settle`] for settlement.
373    ///
374    /// # Errors
375    ///
376    /// Returns [`PaygateError`] if payment processing fails.
377    pub async fn handle_request_fallible<
378        ReqBody,
379        ResBody,
380        S: Service<http::Request<ReqBody>, Response = http::Response<ResBody>>,
381    >(
382        &self,
383        inner: S,
384        req: http::Request<ReqBody>,
385    ) -> Result<Response, PaygateError>
386    where
387        S::Response: IntoResponse,
388        S::Error: IntoResponse,
389        S::Future: Send,
390    {
391        // Step 1: Verify the payment (borrows headers, does not consume req).
392        let verified = self.verify_only(req.headers()).await?;
393
394        // Step 2: Execute the inner handler.
395        let response = match Self::call_inner(inner, req).await {
396            Ok(response) => response,
397            Err(err) => return Ok(err.into_response()),
398        };
399
400        // Step 3: Skip settlement if the handler returned an error response.
401        if response.status().is_client_error() || response.status().is_server_error() {
402            return Ok(response.into_response());
403        }
404
405        // Step 4: Settle and attach the Payment-Response header.
406        let settlement = verified.settle(&self.facilitator).await?;
407        let header_value = settlement_to_header(&settlement)?;
408
409        let mut res = response;
410        res.headers_mut().insert("Payment-Response", header_value);
411        Ok(res.into_response())
412    }
413}
414
415/// Extracts the payment header value from the header map.
416pub fn extract_payment_header<'a>(
417    header_map: &'a HeaderMap,
418    header_name: &'a str,
419) -> Option<&'a [u8]> {
420    header_map.get(header_name).map(HeaderValue::as_bytes)
421}
422
423/// Extracts and deserializes the payment payload from base64-encoded header bytes.
424#[must_use]
425pub fn extract_payment_payload<T>(header_bytes: &[u8]) -> Option<T>
426where
427    T: serde::de::DeserializeOwned,
428{
429    let base64 = Base64Bytes::from(header_bytes).decode().ok()?;
430    let value = serde_json::from_slice(base64.as_ref()).ok()?;
431    Some(value)
432}
433
434/// Converts a **successful** [`proto::SettleResponse`] into an HTTP header value.
435///
436/// Delegates to [`proto::SettleResponse::encode_base64`] for the actual encoding,
437/// which rejects error settlements at the type level.
438///
439/// # Errors
440///
441/// Returns [`PaygateError::Settlement`] if the response is an error variant
442/// or if serialisation / header encoding fails.
443pub fn settlement_to_header(
444    settlement: &proto::SettleResponse,
445) -> Result<HeaderValue, PaygateError> {
446    let encoded = settlement
447        .encode_base64()
448        .ok_or_else(|| PaygateError::Settlement("cannot encode error settlement".to_owned()))?;
449    HeaderValue::from_bytes(encoded.as_ref())
450        .map_err(|err| PaygateError::Settlement(err.to_string()))
451}
452
453/// Constructs a V2 verify request from the payment payload and accepted requirements.
454///
455/// # Errors
456///
457/// Returns [`VerificationError::NoPaymentMatching`] if the accepted price tag is not found
458/// in the accepts list, or a serialization error if the request cannot be converted to JSON.
459pub fn make_verify_request(
460    payment_payload: V2PaymentPayload,
461    accepts: &[v2::PriceTag],
462) -> Result<proto::VerifyRequest, VerificationError> {
463    let accepted = &payment_payload.accepted;
464
465    let selected = accepts
466        .iter()
467        .find(|price_tag| **price_tag == *accepted)
468        .ok_or(VerificationError::NoPaymentMatching)?;
469
470    let verify_request = v2::VerifyRequest {
471        x402_version: v2::V2,
472        payment_payload,
473        payment_requirements: selected.requirements.clone(),
474    };
475
476    let json = serde_json::to_value(&verify_request)
477        .map_err(|e| VerificationError::VerificationFailed(format!("{e}")))?;
478
479    Ok(proto::VerifyRequest::from(json))
480}
481
482/// Validates a verify response, rejecting invalid or unknown variants.
483///
484/// # Errors
485///
486/// Returns [`VerificationError::VerificationFailed`] if the response is invalid or unknown.
487pub fn validate_verify_response(
488    verify_response: proto::VerifyResponse,
489) -> Result<(), VerificationError> {
490    match verify_response {
491        proto::VerifyResponse::Valid { .. } => Ok(()),
492        proto::VerifyResponse::Invalid { reason, .. } => {
493            Err(VerificationError::VerificationFailed(reason))
494        }
495        _ => Err(VerificationError::VerificationFailed(
496            "unknown verify response variant".into(),
497        )),
498    }
499}
500
501/// Converts a [`PaygateError`] into a V2 402 Payment Required HTTP response.
502///
503/// # Panics
504///
505/// Panics if the payment required response cannot be serialized to JSON or if the
506/// HTTP response cannot be constructed. These conditions indicate a bug in the code.
507pub fn error_into_response(
508    err: PaygateError,
509    accepts: &[v2::PriceTag],
510    resource: &v2::ResourceInfo,
511) -> Response {
512    match err {
513        PaygateError::Verification(err) => {
514            let payment_required_response = v2::PaymentRequired {
515                error: Some(err.to_string()),
516                accepts: accepts.iter().map(|pt| pt.requirements.clone()).collect(),
517                x402_version: v2::V2,
518                resource: resource.clone(),
519                extensions: None,
520            };
521            let payment_required_bytes =
522                serde_json::to_vec(&payment_required_response).expect("serialization failed");
523            let payment_required_header = Base64Bytes::encode(&payment_required_bytes);
524            let header_value = HeaderValue::from_bytes(payment_required_header.as_ref())
525                .expect("Failed to create header value");
526
527            Response::builder()
528                .status(StatusCode::PAYMENT_REQUIRED)
529                .header("Payment-Required", header_value)
530                .header("Content-Type", "application/json")
531                .body(Body::from(payment_required_bytes))
532                .expect("Fail to construct response")
533        }
534        PaygateError::Settlement(ref err) => {
535            #[cfg(feature = "telemetry")]
536            tracing::error!(details = %err, "Settlement failed");
537            let body = Body::from(
538                json!({ "error": "Settlement failed", "details": err.clone() }).to_string(),
539            );
540            Response::builder()
541                .status(StatusCode::PAYMENT_REQUIRED)
542                .header("Content-Type", "application/json")
543                .body(body)
544                .expect("Fail to construct response")
545        }
546    }
547}