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    let valid = verify_payment(
239        facilitator,
240        &x_payment_header,
241        &selected,
242        &payment_requirements,
243    )
244    .await?;
245
246    #[cfg(feature = "tracing")]
247    tracing::debug!("Payment verified for payer: {}", valid.payer);
248
249    let settle_response = settle_payment(
250        facilitator,
251        &x_payment_header,
252        &selected,
253        &payment_requirements,
254    )
255    .await?;
256
257    #[cfg(feature = "tracing")]
258    tracing::debug!(
259        "Payment settled: payer='{}', network='{}', transaction='{}'",
260        settle_response.payer,
261        settle_response.network,
262        settle_response.transaction
263    );
264
265    let payer = if settle_response.payer.is_empty() && !valid.payer.is_empty() {
266        valid.payer
267    } else {
268        settle_response.payer
269    };
270
271    Ok(PaymentResponse {
272        success: true,
273        transaction: settle_response.transaction,
274        network: settle_response.network,
275        payer,
276    })
277}
278
279/// Entrypoint for processing a payment, without verification.
280pub async fn process_payment_no_verify<F: Facilitator>(
281    facilitator: &F,
282    headers: &HeaderMap,
283    payment_requirements: Vec<PaymentRequirements>,
284) -> Result<PaymentResponse, ErrorResponse> {
285    let updated_requirements = update_supported_kinds(facilitator, payment_requirements).await?;
286    let x_payment_header = extract_payment_payload(headers, &updated_requirements)?;
287    let selected = select_payment_with_payload(&updated_requirements, &x_payment_header)?;
288
289    let settle_response = settle_payment(
290        facilitator,
291        &x_payment_header,
292        &selected,
293        &updated_requirements,
294    )
295    .await?;
296
297    Ok(PaymentResponse {
298        success: true,
299        transaction: settle_response.transaction,
300        network: settle_response.network,
301        payer: settle_response.payer,
302    })
303}
304
305/// Entrypoint for processing a payment, without settlement.
306pub async fn process_payment_no_settle<F: Facilitator>(
307    facilitator: &F,
308    headers: &HeaderMap,
309    payment_requirements: Vec<PaymentRequirements>,
310) -> Result<FacilitatorVerifyValid, ErrorResponse> {
311    let updated_requirements = update_supported_kinds(facilitator, payment_requirements).await?;
312    let x_payment_header = extract_payment_payload(headers, &updated_requirements)?;
313    let selected = select_payment_with_payload(&updated_requirements, &x_payment_header)?;
314
315    let verify_response = verify_payment(
316        facilitator,
317        &x_payment_header,
318        &selected,
319        &updated_requirements,
320    )
321    .await?;
322
323    Ok(verify_response)
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn test_filter_supported_kinds() {
332        let supported = serde_json::from_str(
333            "{
334  \"kinds\": [
335    {
336      \"x402Version\": 1,
337      \"scheme\": \"exact\",
338      \"network\": \"base-sepolia\"
339    },
340    {
341      \"x402Version\": 1,
342      \"scheme\": \"exact\",
343      \"network\": \"base\"
344    },
345    {
346      \"x402Version\": 1,
347      \"scheme\": \"exact\",
348      \"network\": \"my-mock-network\",
349      \"extra\": {
350        \"mockField\": \"mockValue\"
351      }
352    },
353    {
354      \"x402Version\": 1,
355      \"scheme\": \"exact\",
356      \"network\": \"solana-devnet\",
357      \"extra\": {
358        \"feePayer\": \"2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4\"
359      }
360    },
361    {
362      \"x402Version\": 1,
363      \"scheme\": \"exact\",
364      \"network\": \"solana\",
365      \"extra\": {
366        \"feePayer\": \"2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4\"
367      }
368    }
369  ]
370}",
371        )
372        .unwrap();
373
374        // EVM chains: "supported" doesn't include "extra" field, but payment requirements do
375        let payment_requirements = serde_json::from_value(serde_json::json!([{
376            "scheme": "exact",
377            "network": "base",
378            "maxAmountRequired": "100",
379            "resource": "https://devnet.aimo.network/api/v1/chat/completions",
380            "description": "LLM Generation endpoint",
381            "mimeType": "application/json",
382            "payTo": "0xD14cE79C13CE71a853eF3E8BD75969d4BDEE39c1",
383            "maxTimeoutSeconds": 60,
384            "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
385            "outputSchema": {
386                "input": {
387                    "discoverable": true,
388                    "type": "http",
389                    "method": "POST"
390                }
391            },
392            "extra": {
393                "name": "USD Coin",
394                "version": "2"
395            }
396        }]))
397        .unwrap();
398        // Expect the final filtered item to include the "extra" field from payment requirements
399        let filtered = filter_supported_kinds(&supported, payment_requirements);
400        assert_eq!(filtered.len(), 1);
401        assert_eq!(
402            filtered[0].extra,
403            Some(serde_json::json!({"name": "USD Coin", "version": "2"}))
404        );
405
406        // Solana chains: "supported" includes "extra" field, which should override payment requirements
407        let payment_requirements_solana = serde_json::from_str(
408            "[
409    {
410      \"scheme\": \"exact\",
411      \"network\": \"solana\",
412      \"maxAmountRequired\": \"100\",
413      \"resource\": \"https://devnet.aimo.network/api/v1/chat/completions\",
414      \"description\": \"LLM Generation endpoint\",
415      \"mimeType\": \"application/json\",
416      \"payTo\": \"Ge3jkza5KRfXvaq3GELNLh6V1pjjdEKNpEdGXJgjjKUR\",
417      \"maxTimeoutSeconds\": 60,
418      \"asset\": \"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v\",
419      \"outputSchema\": {
420        \"input\": {
421          \"discoverable\": true,
422          \"type\": \"http\",
423          \"method\": \"POST\"
424        }
425      }
426    }
427  ]",
428        )
429        .unwrap();
430
431        // Expect the final filtered item to include the "extra" field from supported kinds
432        let filtered = filter_supported_kinds(&supported, payment_requirements_solana);
433        assert_eq!(filtered.len(), 1);
434        assert_eq!(
435            filtered[0].extra,
436            Some(serde_json::json!({"feePayer": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4"}))
437        );
438
439        // Mock network: "supported" includes "extra" field, which should override payment requirements
440        let payment_requirements_mock = serde_json::from_str(
441            "[
442    {
443      \"scheme\": \"exact\",
444      \"network\": \"my-mock-network\",
445      \"maxAmountRequired\": \"100\",
446      \"resource\": \"https://devnet.aimo.network/api/v1/chat/completions\",
447      \"description\": \"LLM Generation endpoint\",
448      \"mimeType\": \"application/json\",
449      \"payTo\": \"0xD14cE79C13CE71a853eF3E8BD75969d4BDEE39c1\",
450      \"maxTimeoutSeconds\": 60,
451      \"asset\": \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\",
452      \"outputSchema\": {
453        \"input\": {
454          \"discoverable\": true,
455          \"type\": \"http\",
456          \"method\": \"POST\"
457        }
458      },
459      \"extra\": {
460        \"name\": \"USD Coin\",
461        \"version\": \"2\"
462      }
463    }
464  ]",
465        )
466        .unwrap();
467
468        // Expect the final filtered item to include the "extra" field from supported kinds
469        let filtered = filter_supported_kinds(&supported, payment_requirements_mock);
470        assert_eq!(filtered.len(), 1);
471        assert_eq!(
472            filtered[0].extra,
473            Some(serde_json::json!({"mockField": "mockValue"}))
474        );
475    }
476}