Skip to main content

x402_types/proto/
v1.rs

1//! Protocol version 1 (V1) types for x402.
2//!
3//! This module defines the wire format types for the original x402 protocol version.
4//! V1 uses network names (e.g., "base-sepolia") instead of CAIP-2 chain IDs.
5//!
6//! # Key Types
7//!
8//! - [`X402Version1`] - Version marker that serializes as `1`
9//! - [`PaymentPayload`] - Signed payment authorization from the buyer
10//! - [`PaymentRequirements`] - Payment terms set by the seller
11//! - [`PaymentRequired`] - HTTP 402 response body
12//! - [`VerifyRequest`] / [`VerifyResponse`] - Verification messages
13//! - [`SettleResponse`] - Settlement result
14//! - [`PriceTag`] - Builder for creating payment requirements
15
16use serde::de::DeserializeOwned;
17use serde::{Deserialize, Deserializer, Serialize, Serializer};
18use std::fmt;
19use std::fmt::Display;
20use std::sync::Arc;
21
22use crate::proto;
23use crate::proto::{OriginalJson, SupportedResponse};
24
25/// Version marker for x402 protocol version 1.
26///
27/// This type serializes as the integer `1` and is used to identify V1 protocol
28/// messages in the wire format.
29#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)]
30pub struct X402Version1;
31
32impl X402Version1 {
33    pub const VALUE: u8 = 1;
34}
35
36impl PartialEq<u8> for X402Version1 {
37    fn eq(&self, other: &u8) -> bool {
38        *other == Self::VALUE
39    }
40}
41
42impl From<X402Version1> for u8 {
43    fn from(_: X402Version1) -> Self {
44        X402Version1::VALUE
45    }
46}
47
48impl Serialize for X402Version1 {
49    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
50        serializer.serialize_u8(Self::VALUE)
51    }
52}
53
54impl<'de> Deserialize<'de> for X402Version1 {
55    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
56    where
57        D: Deserializer<'de>,
58    {
59        let num = u8::deserialize(deserializer)?;
60        if num == Self::VALUE {
61            Ok(X402Version1)
62        } else {
63            Err(serde::de::Error::custom(format!(
64                "expected version {}, got {}",
65                Self::VALUE,
66                num
67            )))
68        }
69    }
70}
71
72impl Display for X402Version1 {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        write!(f, "{}", Self::VALUE)
75    }
76}
77
78/// Response from a payment settlement request.
79///
80/// Indicates whether the payment was successfully settled on-chain.
81#[derive(Debug, Clone)]
82pub enum SettleResponse {
83    /// Settlement succeeded.
84    Success {
85        /// The address that paid.
86        payer: String,
87        /// The transaction hash.
88        transaction: String,
89        /// The network where settlement occurred.
90        network: String,
91    },
92    /// Settlement failed.
93    Error {
94        /// The reason for failure.
95        reason: String,
96        /// The network where settlement was attempted.
97        network: String,
98    },
99}
100
101impl From<SettleResponse> for proto::SettleResponse {
102    fn from(val: SettleResponse) -> Self {
103        proto::SettleResponse(
104            serde_json::to_value(val).expect("SettleResponse serialization failed"),
105        )
106    }
107}
108
109#[derive(Serialize, Deserialize)]
110struct SettleResponseWire {
111    pub success: bool,
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub error_reason: Option<String>,
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub payer: Option<String>,
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub transaction: Option<String>,
118    pub network: String,
119}
120
121impl Serialize for SettleResponse {
122    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
123    where
124        S: Serializer,
125    {
126        let wire = match self {
127            SettleResponse::Success {
128                payer,
129                transaction,
130                network,
131            } => SettleResponseWire {
132                success: true,
133                error_reason: None,
134                payer: Some(payer.clone()),
135                transaction: Some(transaction.clone()),
136                network: network.clone(),
137            },
138            SettleResponse::Error { reason, network } => SettleResponseWire {
139                success: false,
140                error_reason: Some(reason.clone()),
141                payer: None,
142                transaction: None,
143                network: network.clone(),
144            },
145        };
146        wire.serialize(serializer)
147    }
148}
149
150impl<'de> Deserialize<'de> for SettleResponse {
151    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
152    where
153        D: Deserializer<'de>,
154    {
155        let wire = SettleResponseWire::deserialize(deserializer)?;
156        match wire.success {
157            true => {
158                let payer = wire
159                    .payer
160                    .ok_or_else(|| serde::de::Error::missing_field("payer"))?;
161                let transaction = wire
162                    .transaction
163                    .ok_or_else(|| serde::de::Error::missing_field("transaction"))?;
164                Ok(SettleResponse::Success {
165                    payer,
166                    transaction,
167                    network: wire.network,
168                })
169            }
170            false => {
171                let reason = wire
172                    .error_reason
173                    .ok_or_else(|| serde::de::Error::missing_field("error_reason"))?;
174                Ok(SettleResponse::Error {
175                    reason,
176                    network: wire.network,
177                })
178            }
179        }
180    }
181}
182
183/// Result returned by a facilitator after verifying a [`PaymentPayload`] against the provided [`PaymentRequirements`].
184///
185/// This response indicates whether the payment authorization is valid and identifies the payer. If invalid,
186/// it includes a reason describing why verification failed (e.g., wrong network, an invalid scheme, insufficient funds).
187#[derive(Debug)]
188pub enum VerifyResponse {
189    /// The payload matches the requirements and passes all checks.
190    Valid { payer: String },
191    /// The payload was well-formed but failed verification due to the specified [`FacilitatorErrorReason`]
192    Invalid {
193        reason: String,
194        payer: Option<String>,
195    },
196}
197
198impl From<VerifyResponse> for proto::VerifyResponse {
199    fn from(val: VerifyResponse) -> Self {
200        proto::VerifyResponse(
201            serde_json::to_value(val).expect("VerifyResponse serialization failed"),
202        )
203    }
204}
205
206impl TryFrom<proto::VerifyResponse> for VerifyResponse {
207    type Error = serde_json::Error;
208    fn try_from(value: proto::VerifyResponse) -> Result<Self, Self::Error> {
209        let json = value.0;
210        Self::deserialize(json)
211    }
212}
213
214impl VerifyResponse {
215    /// Constructs a successful verification response with the given `payer` address.
216    ///
217    /// Indicates that the provided payment payload has been validated against the payment requirements.
218    pub fn valid(payer: String) -> Self {
219        VerifyResponse::Valid { payer }
220    }
221
222    /// Constructs a failed verification response with the given `payer` address and error `reason`.
223    ///
224    /// Indicates that the payment was recognized but rejected due to reasons such as
225    /// insufficient funds, invalid network, or scheme mismatch.
226    #[allow(dead_code)]
227    pub fn invalid(payer: Option<String>, reason: String) -> Self {
228        VerifyResponse::Invalid { reason, payer }
229    }
230}
231
232#[derive(Serialize, Deserialize)]
233#[serde(rename_all = "camelCase")]
234struct VerifyResponseWire {
235    is_valid: bool,
236    #[serde(skip_serializing_if = "Option::is_none")]
237    payer: Option<String>,
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    invalid_reason: Option<String>,
240}
241
242impl Serialize for VerifyResponse {
243    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
244    where
245        S: Serializer,
246    {
247        let wire = match self {
248            VerifyResponse::Valid { payer } => VerifyResponseWire {
249                is_valid: true,
250                payer: Some(payer.clone()),
251                invalid_reason: None,
252            },
253            VerifyResponse::Invalid { reason, payer } => VerifyResponseWire {
254                is_valid: false,
255                payer: payer.clone(),
256                invalid_reason: Some(reason.clone()),
257            },
258        };
259        wire.serialize(serializer)
260    }
261}
262
263impl<'de> Deserialize<'de> for VerifyResponse {
264    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
265    where
266        D: Deserializer<'de>,
267    {
268        let wire = VerifyResponseWire::deserialize(deserializer)?;
269        match wire.is_valid {
270            true => {
271                let payer = wire
272                    .payer
273                    .ok_or_else(|| serde::de::Error::missing_field("payer"))?;
274                Ok(VerifyResponse::Valid { payer })
275            }
276            false => {
277                let reason = wire
278                    .invalid_reason
279                    .ok_or_else(|| serde::de::Error::missing_field("invalid_reason"))?;
280                let payer = wire.payer;
281                Ok(VerifyResponse::Invalid { reason, payer })
282            }
283        }
284    }
285}
286
287/// Request to verify a V1 payment.
288///
289/// Contains the payment payload and requirements for verification.
290#[derive(Debug, Clone, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct VerifyRequest<TPayload, TRequirements> {
293    /// Protocol version (always 1).
294    pub x402_version: X402Version1,
295    /// The signed payment authorization.
296    pub payment_payload: TPayload,
297    /// The payment requirements to verify against.
298    pub payment_requirements: TRequirements,
299}
300
301impl<TPayload, TRequirements> TryFrom<&proto::VerifyRequest>
302    for VerifyRequest<TPayload, TRequirements>
303where
304    Self: DeserializeOwned,
305{
306    type Error = proto::PaymentVerificationError;
307
308    fn try_from(value: &proto::VerifyRequest) -> Result<Self, Self::Error> {
309        let value = serde_json::from_str(value.as_str())?;
310        Ok(value)
311    }
312}
313
314impl<TPayload, TRequirements> TryInto<proto::VerifyRequest>
315    for VerifyRequest<TPayload, TRequirements>
316where
317    TPayload: Serialize,
318    TRequirements: Serialize,
319{
320    type Error = serde_json::Error;
321    fn try_into(self) -> Result<proto::VerifyRequest, Self::Error> {
322        let json = serde_json::to_value(self)?;
323        let raw = serde_json::value::to_raw_value(&json)?;
324        Ok(proto::VerifyRequest(raw))
325    }
326}
327
328/// A signed payment authorization from the buyer.
329///
330/// This contains the cryptographic proof that the buyer has authorized
331/// a payment, along with metadata about the payment scheme and network.
332///
333/// # Type Parameters
334///
335/// - `TScheme` - The scheme identifier type (default: `String`)
336/// - `TPayload` - The scheme-specific payload type (default: raw JSON)
337#[derive(Debug, Clone, Serialize, Deserialize)]
338#[serde(rename_all = "camelCase")]
339pub struct PaymentPayload<TScheme = String, TPayload = Box<serde_json::value::RawValue>> {
340    /// Protocol version (always 1).
341    pub x402_version: X402Version1,
342    /// The payment scheme (e.g., "exact").
343    pub scheme: TScheme,
344    /// The network name (e.g., "base-sepolia").
345    pub network: String,
346    /// The scheme-specific signed payload.
347    pub payload: TPayload,
348}
349
350/// Payment requirements set by the seller.
351///
352/// Defines the terms under which a payment will be accepted, including
353/// the amount, recipient, asset, and timing constraints.
354///
355/// # Type Parameters
356///
357/// - `TScheme` - The scheme identifier type (default: `String`)
358/// - `TAmount` - The amount type (default: `String`)
359/// - `TAddress` - The address type (default: `String`)
360/// - `TExtra` - Scheme-specific extra data type (default: `serde_json::Value`)
361#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
362#[serde(rename_all = "camelCase")]
363pub struct PaymentRequirements<
364    TScheme = String,
365    TAmount = String,
366    TAddress = String,
367    TExtra = serde_json::Value,
368> {
369    /// The payment scheme (e.g., "exact").
370    pub scheme: TScheme,
371    /// The network name (e.g., "base-sepolia").
372    pub network: String,
373    /// The maximum amount required for payment.
374    pub max_amount_required: TAmount,
375    /// The resource URL being paid for.
376    pub resource: String,
377    /// Human-readable description of the resource.
378    pub description: String,
379    /// MIME type of the resource.
380    pub mime_type: Option<String>,
381    /// Optional JSON schema for the resource output.
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub output_schema: Option<serde_json::Value>,
384    /// The recipient address for payment.
385    pub pay_to: TAddress,
386    /// Maximum time in seconds for payment validity.
387    pub max_timeout_seconds: u64,
388    /// The token asset address.
389    pub asset: TAddress,
390    /// Scheme-specific extra data.
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub extra: Option<TExtra>,
393}
394
395impl<TScheme, TAmount, TAddress, TExtra> TryFrom<&OriginalJson>
396    for PaymentRequirements<TScheme, TAmount, TAddress, TExtra>
397where
398    TScheme: for<'a> serde::Deserialize<'a>,
399    TAmount: for<'a> serde::Deserialize<'a>,
400    TAddress: for<'a> serde::Deserialize<'a>,
401    TExtra: for<'a> serde::Deserialize<'a>,
402{
403    type Error = serde_json::Error;
404
405    fn try_from(value: &OriginalJson) -> Result<Self, Self::Error> {
406        let payment_requirements = serde_json::from_str(value.0.get())?;
407        Ok(payment_requirements)
408    }
409}
410
411/// HTTP 402 Payment Required response body for V1.
412///
413/// This is returned when a resource requires payment. It contains
414/// the list of acceptable payment methods.
415#[derive(Debug, Clone, Serialize, Deserialize)]
416#[serde(rename_all = "camelCase")]
417pub struct PaymentRequired<TAccepts = PaymentRequirements> {
418    /// Protocol version (always 1).
419    pub x402_version: X402Version1,
420    /// List of acceptable payment methods.
421    #[serde(default = "Vec::default")]
422    pub accepts: Vec<TAccepts>,
423    /// Optional error message if the request was malformed.
424    #[serde(default, skip_serializing_if = "Option::is_none")]
425    pub error: Option<String>,
426}
427
428/// Builder for creating payment requirements.
429///
430/// A `PriceTag` is a convenient way to specify payment terms that can
431/// be converted into [`PaymentRequirements`] for inclusion in a 402 response.
432///
433/// # Example
434///
435/// ```rust
436/// use x402_types::proto::v1::PriceTag;
437///
438/// let price = PriceTag {
439///     scheme: "exact".to_string(),
440///     pay_to: "0x1234...".to_string(),
441///     asset: "0xUSDC...".to_string(),
442///     network: "base".to_string(),
443///     amount: "1000000".to_string(), // 1 USDC
444///     max_timeout_seconds: 300,
445///     extra: None,
446///     enricher: None,
447/// };
448/// ```
449#[derive(Clone)]
450#[allow(dead_code)] // Public for consumption by downstream crates.
451pub struct PriceTag {
452    /// The payment scheme (e.g., "exact").
453    pub scheme: String,
454    /// The recipient address.
455    pub pay_to: String,
456    /// The token asset address.
457    pub asset: String,
458    /// The network name.
459    pub network: String,
460    /// The payment amount in token units.
461    pub amount: String,
462    /// Maximum time in seconds for payment validity.
463    pub max_timeout_seconds: u64,
464    /// Scheme-specific extra data.
465    pub extra: Option<serde_json::Value>,
466    /// Optional enrichment function for adding facilitator-specific data.
467    #[doc(hidden)]
468    pub enricher: Option<Enricher>,
469}
470
471impl fmt::Debug for PriceTag {
472    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
473        f.debug_struct("PriceTag")
474            .field("scheme", &self.scheme)
475            .field("pay_to", &self.pay_to)
476            .field("asset", &self.asset)
477            .field("network", &self.network)
478            .field("amount", &self.amount)
479            .field("max_timeout_seconds", &self.max_timeout_seconds)
480            .field("extra", &self.extra)
481            .finish()
482    }
483}
484
485/// Enrichment function type for price tags.
486///
487/// Enrichers are called with the facilitator's capabilities to add
488/// facilitator-specific data to price tags (e.g., fee payer addresses).
489pub type Enricher = Arc<dyn Fn(&mut PriceTag, &SupportedResponse) + Send + Sync>;
490
491impl PriceTag {
492    /// Applies the enrichment function if one is set.
493    ///
494    /// This is called automatically when building payment requirements
495    /// to add facilitator-specific data.
496    #[allow(dead_code)]
497    pub fn enrich(&mut self, capabilities: &SupportedResponse) {
498        if let Some(enricher) = self.enricher.clone() {
499            enricher(self, capabilities);
500        }
501    }
502
503    /// Sets the maximum timeout for this price tag.
504    #[allow(dead_code)]
505    pub fn with_timeout(mut self, seconds: u64) -> Self {
506        self.max_timeout_seconds = seconds;
507        self
508    }
509}