x402_kit/seller/
toolkit.rs

1use std::fmt::Display;
2
3use http::{HeaderMap, StatusCode};
4
5use crate::{
6    concepts::Facilitator,
7    transport::{
8        Base64EncodedHeader, FacilitatorPaymentRequest, FacilitatorPaymentRequestPayload,
9        FacilitatorSettleFailed, FacilitatorSettleResponse, FacilitatorSettleSuccess,
10        FacilitatorSupportedResponse, FacilitatorVerifyInvalid, FacilitatorVerifyResponse,
11        FacilitatorVerifyValid, PaymentPayload, PaymentRequirements, PaymentRequirementsResponse,
12        PaymentResponse,
13    },
14    types::X402Version,
15};
16
17/// Structured error response for payment processing.
18#[derive(Debug, Clone)]
19pub struct ErrorResponse {
20    pub status: StatusCode,
21    pub error: String,
22    pub accepts: Vec<PaymentRequirements>,
23}
24
25impl ErrorResponse {
26    pub fn into_payment_requirements_response(self) -> PaymentRequirementsResponse {
27        PaymentRequirementsResponse {
28            x402_version: X402Version::V1,
29            error: self.error,
30            accepts: self.accepts,
31        }
32    }
33
34    pub fn payment_required(accepts: &[PaymentRequirements]) -> Self {
35        ErrorResponse {
36            status: StatusCode::PAYMENT_REQUIRED,
37            error: "X-PAYMENT header is required".to_string(),
38            accepts: accepts.to_owned(),
39        }
40    }
41
42    pub fn invalid_payment(error: impl Display, accepts: &[PaymentRequirements]) -> Self {
43        ErrorResponse {
44            status: StatusCode::BAD_REQUEST,
45            error: error.to_string(),
46            accepts: accepts.to_owned(),
47        }
48    }
49
50    pub fn payment_failed(error: impl Display, accepts: &[PaymentRequirements]) -> Self {
51        ErrorResponse {
52            status: StatusCode::PAYMENT_REQUIRED,
53            error: error.to_string(),
54            accepts: accepts.to_owned(),
55        }
56    }
57
58    pub fn server_error(error: impl Display, accepts: &[PaymentRequirements]) -> Self {
59        ErrorResponse {
60            status: StatusCode::INTERNAL_SERVER_ERROR,
61            error: error.to_string(),
62            accepts: accepts.to_owned(),
63        }
64    }
65}
66
67/// Extracts the payment payload from the raw X-Payment-Header.
68pub fn extract_payment_payload(
69    headers: &HeaderMap,
70    payment_requirements: &[PaymentRequirements],
71) -> Result<Base64EncodedHeader, ErrorResponse> {
72    Ok(Base64EncodedHeader(
73        headers
74            .get("X-Payment")
75            .ok_or(ErrorResponse::payment_required(payment_requirements))?
76            .to_str()
77            .map_err(|err| {
78                ErrorResponse::invalid_payment(
79                    format!("Failed to parse X-Payment header: {}", err),
80                    payment_requirements,
81                )
82            })?
83            .to_string(),
84    ))
85}
86
87/// Updates the payment requirements with supported kinds from the facilitator.
88pub async fn update_supported_kinds<F: Facilitator>(
89    facilitator: &F,
90    payment_requirements: Vec<PaymentRequirements>,
91) -> Result<Vec<PaymentRequirements>, ErrorResponse> {
92    let supported = facilitator
93        .supported()
94        .await
95        .map_err(|err| ErrorResponse::server_error(err, &payment_requirements))?;
96
97    Ok(filter_supported_kinds(&supported, payment_requirements))
98}
99
100/// Filters the payment requirements based on the supported kinds from the facilitator.
101///
102/// Returns only the payment requirements that are supported by the facilitator with updated extra fields.
103pub fn filter_supported_kinds(
104    supported: &FacilitatorSupportedResponse,
105    payment_requirements: Vec<PaymentRequirements>,
106) -> Vec<PaymentRequirements> {
107    payment_requirements
108        .into_iter()
109        .filter_map(|mut pr| {
110            supported
111                .kinds
112                .iter()
113                .find(|kind| kind.scheme == pr.scheme && kind.network == pr.network)
114                .map(|s| {
115                    // Update extra field if present
116                    if s.extra.is_some() {
117                        pr.extra = s.extra.clone();
118                    }
119                    pr
120                })
121        })
122        .collect()
123}
124
125/// Selects the appropriate payment requirements based on the provided payment payload.
126pub fn select_payment_with_payload(
127    payment_requirements: &[PaymentRequirements],
128    x_payment_header: &Base64EncodedHeader,
129) -> Result<PaymentRequirements, ErrorResponse> {
130    let payment_payload = PaymentPayload::try_from(x_payment_header.clone())
131        .map_err(|err| ErrorResponse::invalid_payment(err, payment_requirements))?;
132
133    payment_requirements
134        .iter()
135        .find(|pr| pr.network == payment_payload.network && pr.scheme == payment_payload.scheme)
136        .cloned()
137        .ok_or(ErrorResponse::invalid_payment(
138            "Payment payload does not match any accepted payment requirements",
139            payment_requirements,
140        ))
141}
142
143/// Verifies the payment using the facilitator.
144pub async fn verify_payment<F: Facilitator>(
145    facilitator: &F,
146    x_payment_header: &Base64EncodedHeader,
147    selected: &PaymentRequirements,
148    payment_requirements: &[PaymentRequirements],
149) -> Result<FacilitatorVerifyValid, ErrorResponse> {
150    let payment_payload = x_payment_header
151        .clone()
152        .try_into()
153        .map_err(|err| ErrorResponse::invalid_payment(err, payment_requirements))?;
154
155    #[cfg(feature = "tracing")]
156    tracing::debug!(
157        "Verifying payment for scheme={}, network={}",
158        selected.scheme,
159        selected.network,
160    );
161
162    let request = FacilitatorPaymentRequest {
163        payload: FacilitatorPaymentRequestPayload {
164            payment_payload,
165            payment_requirements: selected.clone(),
166        },
167        x_payment_header: x_payment_header.clone(),
168    };
169
170    let verify_response = facilitator
171        .verify(request)
172        .await
173        .map_err(|err| ErrorResponse::server_error(err, payment_requirements))?;
174
175    match verify_response {
176        FacilitatorVerifyResponse::Valid(valid) => Ok(valid),
177        FacilitatorVerifyResponse::Invalid(FacilitatorVerifyInvalid {
178            invalid_reason,
179            payer,
180        }) => Err(ErrorResponse::invalid_payment(
181            format!(
182                "Invalid payment: reason='{invalid_reason}', payer={}",
183                payer.unwrap_or("[Unknown]".to_string())
184            ),
185            payment_requirements,
186        )),
187    }
188}
189
190/// Settles the payment using the facilitator.
191pub async fn settle_payment<F: Facilitator>(
192    facilitator: &F,
193    x_payment_header: &Base64EncodedHeader,
194    selected: &PaymentRequirements,
195    payment_requirements: &[PaymentRequirements],
196) -> Result<FacilitatorSettleSuccess, ErrorResponse> {
197    let payment_payload = x_payment_header
198        .clone()
199        .try_into()
200        .map_err(|err| ErrorResponse::invalid_payment(err, payment_requirements))?;
201
202    let settle_response: FacilitatorSettleResponse = facilitator
203        .settle(FacilitatorPaymentRequest {
204            payload: FacilitatorPaymentRequestPayload {
205                payment_payload,
206                payment_requirements: selected.clone(),
207            },
208            x_payment_header: x_payment_header.clone(),
209        })
210        .await
211        .map_err(|err| ErrorResponse::server_error(err, payment_requirements))?;
212
213    match settle_response {
214        FacilitatorSettleResponse::Success(success) => Ok(success),
215        FacilitatorSettleResponse::Failed(FacilitatorSettleFailed {
216            error_reason,
217            payer,
218        }) => Err(ErrorResponse::payment_failed(
219            format!(
220                "Payment settlement failed: reason='{}', payer={}",
221                error_reason,
222                payer.unwrap_or("[Unknown]".to_string())
223            ),
224            payment_requirements,
225        )),
226    }
227}
228
229/// Entrypoint for processing a payment.
230pub async fn process_payment<F: Facilitator>(
231    facilitator: &F,
232    headers: &HeaderMap,
233    payment_requirements: Vec<PaymentRequirements>,
234) -> Result<PaymentResponse, ErrorResponse> {
235    let x_payment_header = extract_payment_payload(headers, &payment_requirements)?;
236    let selected = select_payment_with_payload(&payment_requirements, &x_payment_header)?;
237
238    // Allow unused variables when tracing is disabled
239    #[allow(unused_variables)]
240    let valid = verify_payment(
241        facilitator,
242        &x_payment_header,
243        &selected,
244        &payment_requirements,
245    )
246    .await?;
247
248    #[cfg(feature = "tracing")]
249    tracing::debug!("Payment verified for payer: {}", valid.payer);
250
251    let settle_response = settle_payment(
252        facilitator,
253        &x_payment_header,
254        &selected,
255        &payment_requirements,
256    )
257    .await?;
258
259    #[cfg(feature = "tracing")]
260    tracing::debug!(
261        "Payment settled: payer='{}', network='{}', transaction='{}'",
262        settle_response.payer,
263        settle_response.network,
264        settle_response.transaction
265    );
266
267    Ok(PaymentResponse {
268        success: true,
269        transaction: settle_response.transaction,
270        network: settle_response.network,
271        payer: settle_response.payer,
272    })
273}
274
275/// Entrypoint for processing a payment, without verification.
276pub async fn process_payment_no_verify<F: Facilitator>(
277    facilitator: &F,
278    headers: &HeaderMap,
279    payment_requirements: Vec<PaymentRequirements>,
280) -> Result<PaymentResponse, ErrorResponse> {
281    let updated_requirements = update_supported_kinds(facilitator, payment_requirements).await?;
282    let x_payment_header = extract_payment_payload(headers, &updated_requirements)?;
283    let selected = select_payment_with_payload(&updated_requirements, &x_payment_header)?;
284
285    let settle_response = settle_payment(
286        facilitator,
287        &x_payment_header,
288        &selected,
289        &updated_requirements,
290    )
291    .await?;
292
293    Ok(PaymentResponse {
294        success: true,
295        transaction: settle_response.transaction,
296        network: settle_response.network,
297        payer: settle_response.payer,
298    })
299}
300
301/// Entrypoint for processing a payment, without settlement.
302pub async fn process_payment_no_settle<F: Facilitator>(
303    facilitator: &F,
304    headers: &HeaderMap,
305    payment_requirements: Vec<PaymentRequirements>,
306) -> Result<FacilitatorVerifyValid, ErrorResponse> {
307    let updated_requirements = update_supported_kinds(facilitator, payment_requirements).await?;
308    let x_payment_header = extract_payment_payload(headers, &updated_requirements)?;
309    let selected = select_payment_with_payload(&updated_requirements, &x_payment_header)?;
310
311    let verify_response = verify_payment(
312        facilitator,
313        &x_payment_header,
314        &selected,
315        &updated_requirements,
316    )
317    .await?;
318
319    Ok(verify_response)
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn test_filter_supported_kinds() {
328        let supported = serde_json::from_str(
329            "{
330  \"kinds\": [
331    {
332      \"x402Version\": 1,
333      \"scheme\": \"exact\",
334      \"network\": \"base-sepolia\"
335    },
336    {
337      \"x402Version\": 1,
338      \"scheme\": \"exact\",
339      \"network\": \"base\"
340    },
341    {
342      \"x402Version\": 1,
343      \"scheme\": \"exact\",
344      \"network\": \"my-mock-network\",
345      \"extra\": {
346        \"mockField\": \"mockValue\"
347      }
348    },
349    {
350      \"x402Version\": 1,
351      \"scheme\": \"exact\",
352      \"network\": \"solana-devnet\",
353      \"extra\": {
354        \"feePayer\": \"2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4\"
355      }
356    },
357    {
358      \"x402Version\": 1,
359      \"scheme\": \"exact\",
360      \"network\": \"solana\",
361      \"extra\": {
362        \"feePayer\": \"2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4\"
363      }
364    }
365  ]
366}",
367        )
368        .unwrap();
369
370        // EVM chains: "supported" doesn't include "extra" field, but payment requirements do
371        let payment_requirements = serde_json::from_value(serde_json::json!([{
372            "scheme": "exact",
373            "network": "base",
374            "maxAmountRequired": "100",
375            "resource": "https://devnet.aimo.network/api/v1/chat/completions",
376            "description": "LLM Generation endpoint",
377            "mimeType": "application/json",
378            "payTo": "0xD14cE79C13CE71a853eF3E8BD75969d4BDEE39c1",
379            "maxTimeoutSeconds": 60,
380            "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
381            "outputSchema": {
382                "input": {
383                    "discoverable": true,
384                    "type": "http",
385                    "method": "POST"
386                }
387            },
388            "extra": {
389                "name": "USD Coin",
390                "version": "2"
391            }
392        }]))
393        .unwrap();
394        // Expect the final filtered item to include the "extra" field from payment requirements
395        let filtered = filter_supported_kinds(&supported, payment_requirements);
396        assert_eq!(filtered.len(), 1);
397        assert_eq!(
398            filtered[0].extra,
399            Some(serde_json::json!({"name": "USD Coin", "version": "2"}))
400        );
401
402        // Solana chains: "supported" includes "extra" field, which should override payment requirements
403        let payment_requirements_solana = serde_json::from_str(
404            "[
405    {
406      \"scheme\": \"exact\",
407      \"network\": \"solana\",
408      \"maxAmountRequired\": \"100\",
409      \"resource\": \"https://devnet.aimo.network/api/v1/chat/completions\",
410      \"description\": \"LLM Generation endpoint\",
411      \"mimeType\": \"application/json\",
412      \"payTo\": \"Ge3jkza5KRfXvaq3GELNLh6V1pjjdEKNpEdGXJgjjKUR\",
413      \"maxTimeoutSeconds\": 60,
414      \"asset\": \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\",
415      \"outputSchema\": {
416        \"input\": {
417          \"discoverable\": true,
418          \"type\": \"http\",
419          \"method\": \"POST\"
420        }
421      }
422    }
423  ]",
424        )
425        .unwrap();
426
427        // Expect the final filtered item to include the "extra" field from supported kinds
428        let filtered = filter_supported_kinds(&supported, payment_requirements_solana);
429        assert_eq!(filtered.len(), 1);
430        assert_eq!(
431            filtered[0].extra,
432            Some(serde_json::json!({"feePayer": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4"}))
433        );
434
435        // Mock network: "supported" includes "extra" field, which should override payment requirements
436        let payment_requirements_mock = serde_json::from_str(
437            "[
438    {
439      \"scheme\": \"exact\",
440      \"network\": \"my-mock-network\",
441      \"maxAmountRequired\": \"100\",
442      \"resource\": \"https://devnet.aimo.network/api/v1/chat/completions\",
443      \"description\": \"LLM Generation endpoint\",
444      \"mimeType\": \"application/json\",
445      \"payTo\": \"0xD14cE79C13CE71a853eF3E8BD75969d4BDEE39c1\",
446      \"maxTimeoutSeconds\": 60,
447      \"asset\": \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\",
448      \"outputSchema\": {
449        \"input\": {
450          \"discoverable\": true,
451          \"type\": \"http\",
452          \"method\": \"POST\"
453        }
454      },
455      \"extra\": {
456        \"name\": \"USD Coin\",
457        \"version\": \"2\"
458      }
459    }
460  ]",
461        )
462        .unwrap();
463
464        // Expect the final filtered item to include the "extra" field from supported kinds
465        let filtered = filter_supported_kinds(&supported, payment_requirements_mock);
466        assert_eq!(filtered.len(), 1);
467        assert_eq!(
468            filtered[0].extra,
469            Some(serde_json::json!({"mockField": "mockValue"}))
470        );
471    }
472}