forked from coinbase/x402
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
293 lines (229 loc) · 9.43 KB
/
main.py
File metadata and controls
293 lines (229 loc) · 9.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
"""Custom x402 Server Implementation.
This example demonstrates how to implement x402 payment handling manually
without using the pre-built middleware packages like PaymentMiddlewareASGI.
It shows you how the payment flow works under the hood:
1. Check for payment in request headers
2. If no payment, return 402 with payment requirements
3. If payment provided, verify with facilitator
4. Execute handler
5. Settle payment and add settlement headers to response
Use this approach when you need:
- Complete control over the payment flow
- Integration with unsupported frameworks
- Custom error handling or logging
- Understanding of how x402 works internally
"""
import base64
import json
import os
import sys
from dataclasses import dataclass, field
from dotenv import load_dotenv
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
from x402.http import FacilitatorConfig, HTTPFacilitatorClient
from x402.mechanisms.evm.exact import ExactEvmServerScheme
from x402.schemas import Network, PaymentPayload, PaymentRequirements, ResourceConfig
from x402.server import x402ResourceServer
load_dotenv()
# Config
EVM_ADDRESS = os.getenv("EVM_ADDRESS")
EVM_NETWORK: Network = "eip155:84532" # Base Sepolia
FACILITATOR_URL = os.getenv("FACILITATOR_URL", "https://x402.org/facilitator")
if not EVM_ADDRESS:
print("❌ EVM_ADDRESS environment variable is required")
sys.exit(1)
if not FACILITATOR_URL:
print("❌ FACILITATOR_URL environment variable is required")
sys.exit(1)
print("\n🔧 Custom x402 Server Implementation")
print("This example demonstrates manual payment handling without middleware.\n")
print(f"✅ Payment address: {EVM_ADDRESS}")
print(f"✅ Facilitator: {FACILITATOR_URL}\n")
# Create facilitator client and resource server
facilitator_client = HTTPFacilitatorClient(FacilitatorConfig(url=FACILITATOR_URL))
resource_server = x402ResourceServer(facilitator_client).register(
EVM_NETWORK,
ExactEvmServerScheme(),
)
# Route payment configuration
@dataclass
class RoutePaymentConfig:
"""Payment configuration for a route."""
scheme: str
price: str
network: Network
pay_to: str
description: str
mime_type: str
route_configs: dict[str, RoutePaymentConfig] = {
"GET /weather": RoutePaymentConfig(
scheme="exact",
price="$0.001",
network=EVM_NETWORK,
pay_to=EVM_ADDRESS, # type: ignore
description="Weather data",
mime_type="application/json",
),
}
# Cache for built payment requirements
@dataclass
class RouteRequirementsCache:
"""Cache for route payment requirements."""
cache: dict[str, PaymentRequirements] = field(default_factory=dict)
route_requirements = RouteRequirementsCache()
# Create FastAPI app
app = FastAPI(
title="Custom x402 Server",
description="Manual x402 payment handling implementation",
)
@app.middleware("http")
async def custom_payment_middleware(request: Request, call_next) -> Response:
"""Custom payment middleware implementation.
Demonstrates the x402 payment flow:
1. Check for payment in request headers
2. If no payment, return 402 with payment requirements
3. If payment provided, verify with facilitator
4. Execute handler
5. Settle payment and add settlement headers to response
"""
route_key = f"{request.method} {request.url.path}"
route_config = route_configs.get(route_key)
# If route doesn't require payment, continue
if route_config is None:
return await call_next(request)
print(f"📥 Request received: {route_key}")
# Build PaymentRequirements from config (cached for efficiency)
if route_key not in route_requirements.cache:
config = ResourceConfig(
scheme=route_config.scheme,
price=route_config.price,
network=route_config.network,
pay_to=route_config.pay_to,
)
built_requirements = resource_server.build_payment_requirements(config)
if len(built_requirements) == 0:
print("❌ Failed to build payment requirements")
return JSONResponse(
status_code=500,
content={"error": "Server configuration error"},
)
route_requirements.cache[route_key] = built_requirements[0]
requirements = route_requirements.cache[route_key]
# Step 1: Check for payment in headers (v2: PAYMENT-SIGNATURE, v1: X-PAYMENT)
payment_header = request.headers.get("payment-signature") or request.headers.get("x-payment")
if not payment_header:
print("💳 No payment provided, returning 402 Payment Required")
# Step 2: Return 402 with payment requirements
payment_required = resource_server.create_payment_required_response(
[requirements],
resource={
"url": str(request.url),
"description": route_config.description,
"mime_type": route_config.mime_type,
},
)
# Use base64 encoding for the PAYMENT-REQUIRED header (v2 protocol)
requirements_header = base64.b64encode(
json.dumps(payment_required.model_dump(by_alias=True)).encode()
).decode()
return JSONResponse(
status_code=402,
content={
"error": "Payment Required",
"message": "This endpoint requires payment",
},
headers={"PAYMENT-REQUIRED": requirements_header},
)
try:
# Step 3: Verify payment
print("🔐 Payment provided, verifying with facilitator...")
payment_payload_dict = json.loads(base64.b64decode(payment_header).decode("utf-8"))
payment_payload = PaymentPayload.model_validate(payment_payload_dict)
verify_result = await resource_server.verify_payment(payment_payload, requirements)
if not verify_result.is_valid:
print(f"❌ Payment verification failed: {verify_result.invalid_reason}")
return JSONResponse(
status_code=402,
content={
"error": "Invalid Payment",
"reason": verify_result.invalid_reason,
},
)
print("✅ Payment verified successfully")
# Step 4: Execute handler
response = await call_next(request)
# Only settle for successful responses (2xx)
if 200 <= response.status_code < 300:
# Step 5: Settle payment
print("💰 Settling payment on-chain...")
try:
settle_result = await resource_server.settle_payment(payment_payload, requirements)
print(f"✅ Payment settled: {settle_result.transaction}")
# Add settlement headers (v2 protocol uses PAYMENT-RESPONSE)
settlement_header = base64.b64encode(
json.dumps(settle_result.model_dump(by_alias=True)).encode()
).decode()
# Create new response with settlement header
body = b""
async for chunk in response.body_iterator:
body += chunk
return Response(
content=body,
status_code=response.status_code,
headers={**dict(response.headers), "PAYMENT-RESPONSE": settlement_header},
media_type=response.media_type,
)
except Exception as e:
print(f"❌ Settlement failed: {e}")
# Continue with response even if settlement fails
return response
return response
except Exception as e:
print(f"❌ Payment processing error: {e}")
return JSONResponse(
status_code=500,
content={
"error": "Payment Processing Error",
"message": str(e),
},
)
# Routes
@app.get("/health")
async def health_check() -> dict[str, str]:
"""Health check endpoint (no payment required)."""
return {"status": "ok", "version": "2.0.0"}
@app.get("/weather")
async def get_weather(city: str = "San Francisco") -> dict:
"""Protected weather endpoint (requires payment)."""
print("🌤️ Executing weather endpoint handler")
weather_data = {
"San Francisco": {"weather": "foggy", "temperature": 60},
"New York": {"weather": "cloudy", "temperature": 55},
"London": {"weather": "rainy", "temperature": 50},
"Tokyo": {"weather": "clear", "temperature": 65},
}
data = weather_data.get(city, {"weather": "sunny", "temperature": 70})
from datetime import datetime
return {
"city": city,
"weather": data["weather"],
"temperature": data["temperature"],
"timestamp": datetime.now().isoformat(),
}
@app.on_event("startup")
async def startup_event() -> None:
"""Initialize the resource server on startup."""
resource_server.initialize()
print("🚀 Resource server initialized\n")
print("Key implementation steps:")
print(" 1. ✅ Check for payment headers in requests")
print(" 2. ✅ Return 402 with requirements if no payment")
print(" 3. ✅ Verify payments with facilitator")
print(" 4. ✅ Execute handler on successful verification")
print(" 5. ✅ Settle payment and add response headers\n")
print("Test with: curl http://localhost:4021/weather")
print("Or use a client from: ../../clients/\n")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=4021)