Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions examples/python/servers/fastapi/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

<!-- Primary metadata (highest priority for x402scan) -->
<title>Virtuals Protocol - ACP Job Payment</title>
<meta name="description" content="Virtuals Protocol ACP Job Budget Payment Service">
<meta property="og:title" content="Virtuals Protocol ACP">
<meta property="og:description" content="Pay for ACP jobs with x402 protocol">
<meta name="title" content="Virtuals Protocol - ACP Job Payment">
<meta name="description" content="Virtuals Protocol ACP Job Payment Service - Pay for ACP jobs using x402 protocol">

<!-- Open Graph metadata (fallback for x402scan) -->
<meta property="og:title" content="Virtuals Protocol - ACP Job Payment">
<meta property="og:description" content="Pay for ACP jobs with x402 protocol on Virtuals Protocol">
<meta property="og:site_name" content="Virtuals Protocol">
<link rel="icon" href="/favicon.ico">
<meta property="og:type" content="website">

<!-- Favicon (x402scan displays this as the resource icon) -->
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
</head>
<body>
<h1>Virtuals Protocol - ACP Job Payment</h1>
<h1>🤖 Virtuals Protocol - ACP Job Payment</h1>
<p>This is an x402 payment service for Virtuals Protocol ACP jobs.</p>
<p>Status: <span style="color: green;">✓ Operational</span></p>
<hr>
<h2>Available Endpoints:</h2>
<ul>
<li><code>/acp-budget</code> - Pay for ACP job budget (supports dynamic pricing via X-Budget header)</li>
</ul>
</body>
</html>
"""
Expand All @@ -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"])
Expand Down
57 changes: 53 additions & 4 deletions python/x402/src/x402/fastapi/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,17 @@
PaymentPayload,
PaymentRequirements,
Price,
SettleResponse,
x402PaymentRequiredResponse,
PaywallConfig,
SupportedNetworks,
HTTPInputSchema,
)

import os
import httpx


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -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("Custom facilitator settle succeeded")
except Exception as e:
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
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("Failed to extract transactionHash from custom facilitator response: %s", 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(
Expand Down
Loading