x402_types/proto/mod.rs
1//! Protocol types for x402 payment messages.
2//!
3//! This module defines the wire format types used in the x402 protocol for
4//! communication between buyers, sellers, and facilitators. It supports both
5//! protocol version 1 (V1) and version 2 (V2).
6//!
7//! # Protocol Versions
8//!
9//! - **V1** ([`v1`]): Original protocol with network names and simpler structure
10//! - **V2** ([`v2`]): Enhanced protocol with CAIP-2 chain IDs and richer metadata
11//!
12//! # Key Types
13//!
14//! - [`SupportedPaymentKind`] - Describes a payment method supported by a facilitator
15//! - [`SupportedResponse`] - Response from facilitator's `/supported` endpoint
16//! - [`VerifyRequest`] / [`VerifyResponse`] - Payment verification messages
17//! - [`SettleRequest`] / [`SettleResponse`] - Payment settlement messages
18//! - [`PaymentVerificationError`] - Errors that can occur during verification
19//! - [`PaymentProblem`] - Structured error response for payment failures
20//!
21//! # Wire Format
22//!
23//! All types serialize to JSON using camelCase field names. The protocol version
24//! is indicated by the `x402Version` field in payment payloads.
25
26use serde::{Deserialize, Serialize};
27use serde_with::{VecSkipError, serde_as};
28use std::collections::HashMap;
29
30use crate::chain::ChainId;
31use crate::scheme::SchemeHandlerSlug;
32
33pub mod util;
34pub mod v1;
35pub mod v2;
36
37/// Trait for types that have both V1 and V2 protocol variants.
38///
39/// This trait enables generic handling of protocol-versioned types through
40/// the [`ProtocolVersioned`] enum.
41pub trait ProtocolV {
42 /// The V1 protocol variant of this type.
43 type V1;
44 /// The V2 protocol variant of this type.
45 type V2;
46}
47
48/// A versioned protocol type that can be either V1 or V2.
49///
50/// This enum wraps protocol-specific types to allow handling both versions
51/// in a unified way.
52pub enum ProtocolVersioned<T>
53where
54 T: ProtocolV,
55{
56 /// Protocol version 1 variant.
57 #[allow(dead_code)]
58 V1(T::V1),
59 /// Protocol version 2 variant.
60 #[allow(dead_code)]
61 V2(T::V2),
62}
63
64/// Describes a payment method supported by a facilitator.
65///
66/// This type is returned in the [`SupportedResponse`] to indicate what
67/// payment schemes, networks, and protocol versions a facilitator can handle.
68///
69/// # Example
70///
71/// ```json
72/// {
73/// "x402Version": 2,
74/// "scheme": "exact",
75/// "network": "eip155:8453"
76/// }
77/// ```
78#[derive(Clone, Debug, Serialize, Deserialize)]
79#[serde(rename_all = "camelCase")]
80pub struct SupportedPaymentKind {
81 /// The x402 protocol version (1 or 2).
82 pub x402_version: u8,
83 /// The payment scheme identifier (e.g., "exact").
84 pub scheme: String,
85 /// The network identifier (CAIP-2 chain ID for V2, network name for V1).
86 pub network: String,
87 /// Optional scheme-specific extra data.
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub extra: Option<serde_json::Value>,
90}
91
92/// Response from a facilitator's `/supported` endpoint.
93///
94/// This response tells clients what payment methods the facilitator supports,
95/// including protocol versions, schemes, networks, and signer addresses.
96///
97/// # Example
98///
99/// ```json
100/// {
101/// "kinds": [
102/// { "x402Version": 2, "scheme": "exact", "network": "eip155:8453" }
103/// ],
104/// "extensions": [],
105/// "signers": {
106/// "eip155:8453": ["0x1234..."]
107/// }
108/// }
109/// ```
110#[serde_as]
111#[derive(Clone, Default, Debug, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase")]
113#[allow(dead_code)] // Public for consumption by downstream crates.
114pub struct SupportedResponse {
115 /// List of supported payment kinds.
116 #[serde_as(as = "VecSkipError<_>")]
117 pub kinds: Vec<SupportedPaymentKind>,
118 /// List of supported protocol extensions.
119 #[serde(default)]
120 pub extensions: Vec<String>,
121 /// Map of chain IDs to signer addresses for that chain.
122 #[serde(default)]
123 pub signers: HashMap<ChainId, Vec<String>>,
124}
125
126/// Request to verify a payment before settlement.
127///
128/// This wrapper contains the payment payload and requirements sent by a client
129/// to a facilitator for verification. The facilitator checks that the payment
130/// authorization is valid, properly signed, and matches the requirements.
131///
132/// The inner JSON structure varies by protocol version and scheme.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct VerifyRequest(Box<serde_json::value::RawValue>);
135
136/// Request to settle a verified payment on-chain.
137///
138/// This is the same structure as [`VerifyRequest`], containing the payment
139/// payload that was previously verified.
140pub type SettleRequest = VerifyRequest;
141
142impl From<Box<serde_json::value::RawValue>> for VerifyRequest {
143 fn from(value: Box<serde_json::value::RawValue>) -> Self {
144 Self(value)
145 }
146}
147
148impl VerifyRequest {
149 pub fn as_str(&self) -> &str {
150 self.0.get()
151 }
152
153 /// Extracts the scheme handler slug from the request.
154 ///
155 /// This determines which scheme handler should process this payment
156 /// based on the protocol version, chain ID, and scheme name.
157 ///
158 /// Returns `None` if the request format is invalid or the scheme is unknown.
159 pub fn scheme_handler_slug(&self) -> Option<SchemeHandlerSlug> {
160 #[derive(Debug, Deserialize, Serialize)]
161 #[serde(untagged)]
162 enum VerifyRequestWire {
163 #[serde(rename_all = "camelCase")]
164 V1 {
165 x402_version: v1::X402Version1,
166 payment_payload: PaymentPayloadV1,
167 },
168 #[serde(rename_all = "camelCase")]
169 V2 {
170 x402_version: v2::X402Version2,
171 payment_payload: PaymentPayloadV2,
172 },
173 }
174
175 #[derive(Debug, Deserialize, Serialize)]
176 #[serde(rename_all = "camelCase")]
177 struct PaymentPayloadV1 {
178 pub network: String,
179 pub scheme: String,
180 }
181
182 #[derive(Debug, Deserialize, Serialize)]
183 #[serde(rename_all = "camelCase")]
184 struct PaymentPayloadV2 {
185 pub accepted: PaymentPayloadV2Accepted,
186 }
187
188 #[derive(Debug, Deserialize, Serialize)]
189 #[serde(rename_all = "camelCase")]
190 struct PaymentPayloadV2Accepted {
191 pub network: ChainId,
192 pub scheme: String,
193 }
194
195 let wire = serde_json::from_str::<VerifyRequestWire>(self.as_str()).ok()?;
196 match wire {
197 VerifyRequestWire::V1 {
198 payment_payload,
199 x402_version,
200 } => {
201 let network_name = payment_payload.network;
202 let chain_id = ChainId::from_network_name(&network_name)?;
203 let scheme = payment_payload.scheme;
204 let slug = SchemeHandlerSlug::new(chain_id, x402_version.into(), scheme);
205 Some(slug)
206 }
207 VerifyRequestWire::V2 {
208 payment_payload,
209 x402_version,
210 } => {
211 let chain_id = payment_payload.accepted.network;
212 let scheme = payment_payload.accepted.scheme;
213 let slug = SchemeHandlerSlug::new(chain_id, x402_version.into(), scheme);
214 Some(slug)
215 }
216 }
217 }
218}
219
220/// Response from a payment verification request.
221///
222/// Contains the verification result as JSON. The structure varies by
223/// protocol version and scheme.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct VerifyResponse(pub serde_json::Value);
226
227/// Response from a payment settlement request.
228///
229/// Contains the settlement result as JSON, typically including the
230/// transaction hash if settlement was successful.
231#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct SettleResponse(pub serde_json::Value);
233
234/// Errors that can occur during payment verification.
235///
236/// These errors are returned when a payment fails validation checks
237/// performed by the facilitator before settlement.
238#[derive(Debug, thiserror::Error)]
239pub enum PaymentVerificationError {
240 /// The payment payload format is invalid or malformed.
241 #[error("Invalid format: {0}")]
242 InvalidFormat(String),
243 /// The payment amount doesn't match the requirements.
244 #[error("Payment amount is invalid with respect to the payment requirements")]
245 InvalidPaymentAmount,
246 /// The payment authorization's `validAfter` timestamp is in the future.
247 #[error("Payment authorization is not yet valid")]
248 Early,
249 /// The payment authorization's `validBefore` timestamp has passed.
250 #[error("Payment authorization is expired")]
251 Expired,
252 /// The payment's chain ID doesn't match the requirements.
253 #[error("Payment chain id is invalid with respect to the payment requirements")]
254 ChainIdMismatch,
255 /// The payment recipient doesn't match the requirements.
256 #[error("Payment recipient is invalid with respect to the payment requirements")]
257 RecipientMismatch,
258 /// The payment asset (token) doesn't match the requirements.
259 #[error("Payment asset is invalid with respect to the payment requirements")]
260 AssetMismatch,
261 /// The payer's on-chain balance is insufficient.
262 #[error("Onchain balance is not enough to cover the payment amount")]
263 InsufficientFunds,
264 #[error("Allowance is not enough to cover the payment amount")]
265 InsufficientAllowance,
266 /// The payment signature is invalid.
267 #[error("{0}")]
268 InvalidSignature(String),
269 /// Transaction simulation failed.
270 #[error("{0}")]
271 TransactionSimulation(String),
272 /// The chain is not supported by this facilitator.
273 #[error("Unsupported chain")]
274 UnsupportedChain,
275 /// The payment scheme is not supported by this facilitator.
276 #[error("Unsupported scheme")]
277 UnsupportedScheme,
278 /// The accepted payment details don't match the requirements.
279 #[error("Accepted does not match payment requirements")]
280 AcceptedRequirementsMismatch,
281}
282
283impl AsPaymentProblem for PaymentVerificationError {
284 fn as_payment_problem(&self) -> PaymentProblem {
285 let error_reason = match self {
286 PaymentVerificationError::InvalidFormat(_) => ErrorReason::InvalidFormat,
287 PaymentVerificationError::InvalidPaymentAmount => ErrorReason::InvalidPaymentAmount,
288 PaymentVerificationError::InsufficientFunds => ErrorReason::InsufficientFunds,
289 PaymentVerificationError::InsufficientAllowance => {
290 ErrorReason::Permit2AllowanceRequired
291 }
292 PaymentVerificationError::Early => ErrorReason::InvalidPaymentEarly,
293 PaymentVerificationError::Expired => ErrorReason::InvalidPaymentExpired,
294 PaymentVerificationError::ChainIdMismatch => ErrorReason::ChainIdMismatch,
295 PaymentVerificationError::RecipientMismatch => ErrorReason::RecipientMismatch,
296 PaymentVerificationError::AssetMismatch => ErrorReason::AssetMismatch,
297 PaymentVerificationError::InvalidSignature(_) => ErrorReason::InvalidSignature,
298 PaymentVerificationError::TransactionSimulation(_) => {
299 ErrorReason::TransactionSimulation
300 }
301 PaymentVerificationError::UnsupportedChain => ErrorReason::UnsupportedChain,
302 PaymentVerificationError::UnsupportedScheme => ErrorReason::UnsupportedScheme,
303 PaymentVerificationError::AcceptedRequirementsMismatch => {
304 ErrorReason::AcceptedRequirementsMismatch
305 }
306 };
307 PaymentProblem::new(error_reason, self.to_string())
308 }
309}
310
311impl From<serde_json::Error> for PaymentVerificationError {
312 fn from(value: serde_json::Error) -> Self {
313 Self::InvalidFormat(value.to_string())
314 }
315}
316
317/// Machine-readable error reason codes for payment failures.
318///
319/// These codes are used in error responses to allow clients to
320/// programmatically handle different failure scenarios.
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
322#[serde(rename_all = "snake_case")]
323pub enum ErrorReason {
324 /// The payment payload format is invalid.
325 InvalidFormat,
326 /// The payment amount is incorrect.
327 InvalidPaymentAmount,
328 /// The payment authorization is not yet valid.
329 InvalidPaymentEarly,
330 /// The payment authorization has expired.
331 InvalidPaymentExpired,
332 /// The chain ID doesn't match.
333 ChainIdMismatch,
334 /// The recipient address doesn't match.
335 RecipientMismatch,
336 /// The token asset doesn't match.
337 AssetMismatch,
338 /// The accepted details don't match requirements.
339 AcceptedRequirementsMismatch,
340 /// The signature is invalid.
341 InvalidSignature,
342 /// Transaction simulation failed.
343 TransactionSimulation,
344 /// Insufficient on-chain balance.
345 InsufficientFunds,
346 /// Insufficient allowance.
347 Permit2AllowanceRequired,
348 /// The chain is not supported.
349 UnsupportedChain,
350 /// The scheme is not supported.
351 UnsupportedScheme,
352 /// An unexpected error occurred.
353 UnexpectedError,
354}
355
356/// Trait for converting errors into structured payment problems.
357pub trait AsPaymentProblem {
358 /// Converts this error into a [`PaymentProblem`].
359 fn as_payment_problem(&self) -> PaymentProblem;
360}
361
362/// A structured payment error with reason code and details.
363///
364/// This type is used to return detailed error information to clients
365/// when a payment fails verification or settlement.
366pub struct PaymentProblem {
367 /// The machine-readable error reason.
368 reason: ErrorReason,
369 /// Human-readable error details.
370 details: String,
371}
372
373impl PaymentProblem {
374 /// Creates a new payment problem with the given reason and details.
375 pub fn new(reason: ErrorReason, details: String) -> Self {
376 Self { reason, details }
377 }
378
379 /// Returns the error reason code.
380 pub fn reason(&self) -> ErrorReason {
381 self.reason
382 }
383
384 /// Returns the human-readable error details.
385 pub fn details(&self) -> &str {
386 &self.details
387 }
388}
389
390/// Protocol version marker for [`PaymentRequired`] responses.
391pub struct PaymentRequiredV;
392
393impl ProtocolV for PaymentRequiredV {
394 type V1 = v1::PaymentRequired<OriginalJson>;
395 type V2 = v2::PaymentRequired<OriginalJson>;
396}
397
398/// A payment required response that can be either V1 or V2.
399///
400/// This is returned with HTTP 402 status to indicate that payment is required.
401pub type PaymentRequired = ProtocolVersioned<PaymentRequiredV>;
402
403/// Verbatim JSON for PaymentRequirements and other places.
404#[derive(Debug, Serialize, Deserialize, Clone)]
405pub struct OriginalJson(pub Box<serde_json::value::RawValue>);