From 8bf12c48adaa015d79fc0098370e5d29b408c53f Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Tue, 4 Nov 2025 22:40:45 +0800 Subject: [PATCH 1/4] feat: custom facilitator --- python/x402/src/x402/fastapi/middleware.py | 57 ++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/python/x402/src/x402/fastapi/middleware.py b/python/x402/src/x402/fastapi/middleware.py index de601bc66..6c653d558 100644 --- a/python/x402/src/x402/fastapi/middleware.py +++ b/python/x402/src/x402/fastapi/middleware.py @@ -20,12 +20,17 @@ PaymentPayload, PaymentRequirements, Price, + SettleResponse, x402PaymentRequiredResponse, PaywallConfig, SupportedNetworks, HTTPInputSchema, ) +import os +import httpx + + logger = logging.getLogger(__name__) @@ -206,10 +211,54 @@ def x402_response(error: str): # Settle the payment try: - - settle_response = await facilitator.settle( - payment, selected_payment_requirements - ) + custom_facilitator_url = os.getenv("CUSTOM_FACILITATOR_API_URL", "") + custom_facilitator_succeeded = False + custom_facilitator_response = None + + if custom_facilitator_url: + try: + url = f"{custom_facilitator_url.rstrip('/')}/api/x402/facilitators/settle" + headers = {"x-payment": payment_header} + async with httpx.AsyncClient(timeout=60.0) as client: + custom_facilitator_response = await client.post(url, headers=headers) + custom_facilitator_response.raise_for_status() + custom_facilitator_succeeded = True + logger.info(f"Custom facilitator settle succeeded") + except Exception as e: + logger.warning(f"Custom facilitator settle POST failed: {e}, falling back to default facilitator") + custom_facilitator_succeeded = False + + # Fallback to default facilitator if custom facilitator wasn't used or failed + if not custom_facilitator_succeeded: + settle_response = await facilitator.settle( + payment, selected_payment_requirements + ) + else: + # If custom facilitator succeeded, we still need a settle_response for the response headers + # Create a minimal success response + try: + payer = payment.payload.authorization.from_ + except (AttributeError, KeyError): + payer = "" + + # Extract transaction hash from custom facilitator response + transaction_hash = "" + try: + if custom_facilitator_response: + response_data = custom_facilitator_response.json() + if isinstance(response_data, dict) and "data" in response_data: + data = response_data["data"] + if isinstance(data, dict) and "transactionHash" in data: + transaction_hash = data["transactionHash"] + except (AttributeError, KeyError, ValueError, httpx.DecodeError) as e: + logger.warning(f"Failed to extract transactionHash from custom facilitator response: {e}") + + settle_response = SettleResponse( + success=True, + transaction=transaction_hash, + network=selected_payment_requirements.network, + payer=payer, + ) if settle_response.success: response.headers["X-PAYMENT-RESPONSE"] = base64.b64encode( From 1dd5bd7d9d0492ab0e58c251d2c2ff2f40edd084 Mon Sep 17 00:00:00 2001 From: koo-virtuals Date: Wed, 5 Nov 2025 09:42:33 +0800 Subject: [PATCH 2/4] support server icon and title --- examples/python/servers/fastapi/main.py | 36 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/examples/python/servers/fastapi/main.py b/examples/python/servers/fastapi/main.py index cecdad2f0..0645e144d 100644 --- a/examples/python/servers/fastapi/main.py +++ b/examples/python/servers/fastapi/main.py @@ -161,27 +161,42 @@ async def dynamic_price_middleware(request: Request, call_next): @app.get("/") async def health_check(): - """Health check endpoint that also serves as site metadata""" + """Health check endpoint that also serves as site metadata for x402scan""" from fastapi.responses import HTMLResponse - # Return HTML with meta tags for better x402scan integration + # Return HTML with meta tags for x402scan to scrape + # x402scan extracts: title, description, favicon, og:* tags html_content = """ + + Virtuals Protocol - ACP Job Payment - - - + + + + + + - + + + + + -

Virtuals Protocol - ACP Job Payment

+

🤖 Virtuals Protocol - ACP Job Payment

This is an x402 payment service for Virtuals Protocol ACP jobs.

Status: ✓ Operational

+
+

Available Endpoints:

+ """ @@ -191,11 +206,14 @@ async def health_check(): @app.get("/favicon.ico") async def favicon(): """Serve favicon for x402scan to display as resource icon""" + from fastapi.responses import Response + favicon_path = os.path.join(os.path.dirname(__file__), "static", "favicon.ico") if os.path.exists(favicon_path): return FileResponse(favicon_path, media_type="image/x-icon") - # Return a default response if favicon doesn't exist - return FileResponse(favicon_path, media_type="image/x-icon", status_code=404) + + # Return 404 if favicon doesn't exist + return Response(status_code=404, content="Favicon not found") @app.api_route("/acp-budget", methods=["GET", "POST"]) From f68ab38f4edcb0fc553d6792555f51eb06f533fe Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Wed, 5 Nov 2025 09:50:25 +0800 Subject: [PATCH 3/4] fix: remove f prefix --- python/x402/src/x402/fastapi/middleware.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/x402/src/x402/fastapi/middleware.py b/python/x402/src/x402/fastapi/middleware.py index 6c653d558..71f3201c1 100644 --- a/python/x402/src/x402/fastapi/middleware.py +++ b/python/x402/src/x402/fastapi/middleware.py @@ -223,9 +223,9 @@ def x402_response(error: str): custom_facilitator_response = await client.post(url, headers=headers) custom_facilitator_response.raise_for_status() custom_facilitator_succeeded = True - logger.info(f"Custom facilitator settle succeeded") + logger.info("Custom facilitator settle succeeded") except Exception as e: - logger.warning(f"Custom facilitator settle POST failed: {e}, falling back to default facilitator") + logger.warning("Custom facilitator settle POST failed: {e}, falling back to default facilitator") custom_facilitator_succeeded = False # Fallback to default facilitator if custom facilitator wasn't used or failed @@ -251,7 +251,7 @@ def x402_response(error: str): if isinstance(data, dict) and "transactionHash" in data: transaction_hash = data["transactionHash"] except (AttributeError, KeyError, ValueError, httpx.DecodeError) as e: - logger.warning(f"Failed to extract transactionHash from custom facilitator response: {e}") + logger.warning("Failed to extract transactionHash from custom facilitator response: {e}") settle_response = SettleResponse( success=True, From f6e2e0722bcb9604d7ae71fdc6a95013e3201724 Mon Sep 17 00:00:00 2001 From: Andrew Khor Date: Wed, 5 Nov 2025 09:52:58 +0800 Subject: [PATCH 4/4] fix: error log --- python/x402/src/x402/fastapi/middleware.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/x402/src/x402/fastapi/middleware.py b/python/x402/src/x402/fastapi/middleware.py index 71f3201c1..618fb3e94 100644 --- a/python/x402/src/x402/fastapi/middleware.py +++ b/python/x402/src/x402/fastapi/middleware.py @@ -225,7 +225,7 @@ def x402_response(error: str): custom_facilitator_succeeded = True logger.info("Custom facilitator settle succeeded") except Exception as e: - logger.warning("Custom facilitator settle POST failed: {e}, falling back to default facilitator") + logger.warning("Custom facilitator settle POST failed: %s, falling back to default facilitator", e) custom_facilitator_succeeded = False # Fallback to default facilitator if custom facilitator wasn't used or failed @@ -251,7 +251,7 @@ def x402_response(error: str): if isinstance(data, dict) and "transactionHash" in data: transaction_hash = data["transactionHash"] except (AttributeError, KeyError, ValueError, httpx.DecodeError) as e: - logger.warning("Failed to extract transactionHash from custom facilitator response: {e}") + logger.warning("Failed to extract transactionHash from custom facilitator response: %s", e) settle_response = SettleResponse( success=True,