Make token and chain settings configurable via .env#3
Conversation
- Add TOKEN_ADDRESS, TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS, TOKEN_VERSION - Add CHAIN_ID and EXPLORER_URL settings - Add /config endpoint to expose settings to frontend - Update index.html to fetch config from server instead of hardcoding - Add test for /config endpoint Co-Authored-By: Claude <noreply@anthropic.com>
- Remove "Create Payment Link" section from index.html - Create new create-payment-link.html with: - API payment link (for x402 clients) - Browser payment link (/index.html?pid=<id>) - Status check link - Copy to clipboard buttons - index.html now only handles Pay and Check Status Co-Authored-By: Claude <noreply@anthropic.com>
The token version is obtained from the x402 payment requirements response (req.extra?.version), not from server config. Co-Authored-By: Claude <noreply@anthropic.com>
- .env.example.base-sepolia: Base Sepolia testnet config - .env.example.base-mainnet: Base mainnet config Co-Authored-By: Claude <noreply@anthropic.com>
- Add /create route to serve create-payment-link.html - Add link to create page at bottom of index.html Co-Authored-By: Claude <noreply@anthropic.com>
- Add console.log statements to debug X-Payment header issue - Fix potential bug where response body could be consumed twice - Explicitly set GET method and Accept header on payment submission Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Debug output will show: - All received headers - Whether x-payment header is present - Header value length if present Co-Authored-By: Claude <noreply@anthropic.com>
- Replace text "Copy" button with clipboard icon - Position copy icon at top-right of each link box - Use monospace font for links - Add visual feedback: icon turns to checkmark when copied - Add link to payment page at bottom Co-Authored-By: Claude <noreply@anthropic.com>
Add !important to override the .connected class color Co-Authored-By: Claude <noreply@anthropic.com>
x402 library expects 'X-Payment' but FastAPI lowercases to 'x-payment'. Copy the value to both keys to ensure compatibility. Co-Authored-By: Claude <noreply@anthropic.com>
The x402 library's FacilitatorClient.settle() doesn't set a timeout, defaulting to httpx's 5-second timeout. Blockchain transactions take longer, causing ReadTimeout errors. Patch httpx.AsyncClient to use 60-second timeout for settle requests. Also clean up debug logging from previous commits. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds configuration for token and chain settings via environment variables and creates a separate UI page for payment link creation. The main goal is to make the service more flexible by allowing custom token configurations beyond the hardcoded USDC values.
Changes:
- Added configurable token settings (TOKEN_ADDRESS, TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS, TOKEN_VERSION) and chain settings (CHAIN_ID, EXPLORER_URL) to config.py
- Created new
/configAPI endpoint to expose configuration to the frontend - Separated payment creation UI into a new page at
/createendpoint - Updated frontend to load server configuration instead of using hardcoded values
- Added test coverage for the new
/configendpoint
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| config.py | Added token and chain configuration settings with defaults for Base Sepolia |
| main.py | Added /config and /create endpoints, header normalization, httpx timeout patching |
| tests/test_main.py | Added test for /config endpoint validating returned configuration structure |
| static/index.html | Removed payment creation UI, added config loading, updated to use server config for explorer URLs |
| static/create-payment-link.html | New standalone page for creating payment links with improved UX |
| .env.example.base-sepolia | Added token and chain configuration settings for testnet |
| .env.example.base-mainnet | New example file with mainnet configuration |
Comments suppressed due to low confidence (1)
.env.example.base-sepolia:25
- The PR description lists TOKEN_VERSION as one of the new .env settings with example values, but TOKEN_VERSION is missing from both .env.example files (.env.example.base-sepolia and .env.example.base-mainnet).
Either add TOKEN_VERSION to these files to match the PR description, or remove it from the PR description if it's not intended to be user-configurable.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Load server configuration | ||
| async function loadConfig() { | ||
| try { | ||
| const response = await fetch('/config'); | ||
| if (response.ok) { | ||
| serverConfig = await response.json(); | ||
| console.log('Server config loaded:', serverConfig); | ||
| } else { | ||
| console.error('Failed to load config:', response.status); | ||
| } | ||
| } catch (error) { | ||
| console.error('Error loading config:', error); | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
The loadConfig function silently fails if the /config endpoint is unavailable or returns an error. This means serverConfig will remain null, which causes a cryptic "Server config not loaded" error only when the user tries to make a payment (line 257).
Consider improving the error handling:
- Show a prominent warning to the user if config fails to load on page initialization
- Retry loading the config if it fails
- At minimum, provide more detailed error information in the console or UI so users understand what went wrong
This is particularly important since the config is now required for the payment flow to work.
| // Load server configuration | |
| async function loadConfig() { | |
| try { | |
| const response = await fetch('/config'); | |
| if (response.ok) { | |
| serverConfig = await response.json(); | |
| console.log('Server config loaded:', serverConfig); | |
| } else { | |
| console.error('Failed to load config:', response.status); | |
| } | |
| } catch (error) { | |
| console.error('Error loading config:', error); | |
| } | |
| } | |
| // Show a prominent banner if server config fails to load | |
| function showConfigErrorBanner(message) { | |
| try { | |
| let banner = document.getElementById('config-error-banner'); | |
| if (!banner) { | |
| banner = document.createElement('div'); | |
| banner.id = 'config-error-banner'; | |
| banner.style.position = 'fixed'; | |
| banner.style.top = '0'; | |
| banner.style.left = '0'; | |
| banner.style.right = '0'; | |
| banner.style.zIndex = '9999'; | |
| banner.style.padding = '12px 16px'; | |
| banner.style.backgroundColor = '#b91c1c'; // dark red | |
| banner.style.color = '#ffffff'; | |
| banner.style.textAlign = 'center'; | |
| banner.style.fontFamily = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'; | |
| banner.style.fontSize = '14px'; | |
| banner.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.2)'; | |
| banner.style.display = 'block'; | |
| document.body.appendChild(banner); | |
| } | |
| banner.textContent = message; | |
| banner.style.display = 'block'; | |
| } catch (e) { | |
| // As a last resort, log to console if DOM manipulation fails | |
| console.error('Unable to display config error banner:', e, 'Original message:', message); | |
| } | |
| } | |
| // Load server configuration | |
| async function loadConfig(retryCount = 3, retryDelayMs = 2000) { | |
| let lastError = null; | |
| for (let attempt = 1; attempt <= retryCount; attempt++) { | |
| try { | |
| const response = await fetch('/config'); | |
| if (response.ok) { | |
| serverConfig = await response.json(); | |
| console.log('Server config loaded:', serverConfig); | |
| return true; | |
| } else { | |
| const errorText = await response.text().catch(() => ''); | |
| lastError = new Error( | |
| `Failed to load config (attempt ${attempt}/${retryCount}): ` + | |
| `HTTP ${response.status} ${response.statusText || ''} ` + | |
| (errorText ? `- ${errorText}` : '') | |
| ); | |
| console.error(lastError); | |
| } | |
| } catch (error) { | |
| lastError = error; | |
| console.error(`Error loading config (attempt ${attempt}/${retryCount}):`, error); | |
| } | |
| // If not the last attempt, wait before retrying | |
| if (attempt < retryCount) { | |
| await new Promise(resolve => setTimeout(resolve, retryDelayMs)); | |
| } | |
| } | |
| // All attempts failed: show a prominent warning to the user | |
| const userMessage = | |
| 'Unable to load server configuration. Payments may not work until the page can contact the server. ' + | |
| 'Please check your connection and try reloading the page.'; | |
| showConfigErrorBanner(userMessage); | |
| if (lastError) { | |
| console.error('Config load ultimately failed after retries:', lastError); | |
| } else { | |
| console.error('Config load ultimately failed after retries for an unknown reason.'); | |
| } | |
| return false; | |
| } |
| # Token settings | ||
| self.token_address: str = os.getenv( | ||
| "TOKEN_ADDRESS", "0x036CbD53842c5426634e7929541eC2318f3dCF7e" | ||
| ) | ||
| self.token_name: str = os.getenv("TOKEN_NAME", "USD Coin") | ||
| self.token_symbol: str = os.getenv("TOKEN_SYMBOL", "USDC") | ||
| self.token_decimals: int = int(os.getenv("TOKEN_DECIMALS", "6")) | ||
|
|
||
| # Chain settings | ||
| self.chain_id: int = int(os.getenv("CHAIN_ID", "84532")) | ||
| self.explorer_url: str = os.getenv( | ||
| "EXPLORER_URL", "https://sepolia.basescan.org/tx/" | ||
| ) |
There was a problem hiding this comment.
The new token and chain configuration settings (TOKEN_ADDRESS, TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS, TOKEN_VERSION, CHAIN_ID, EXPLORER_URL) are not documented in the README's Configuration section. The README only lists the original settings and doesn't mention these new configurable options.
Update the Configuration section in README.md to include these new environment variables with their descriptions and defaults.
| JSON with network, token, and chain configuration. | ||
| """ | ||
| return { | ||
| "network": settings.network, | ||
| "tokenAddress": settings.token_address, | ||
| "tokenName": settings.token_name, | ||
| "tokenSymbol": settings.token_symbol, | ||
| "tokenDecimals": settings.token_decimals, |
There was a problem hiding this comment.
The new token configuration settings (token_address, token_name, token_symbol, token_decimals) are exposed via the /config endpoint but are not actually used by the backend PaymentService initialization (lines 201-212). The PaymentService only receives the network parameter, which likely determines the token settings internally.
This creates a potential inconsistency where:
- The frontend shows one set of token settings from /config
- The backend uses potentially different token settings from the x402 library based on the network parameter
Either:
- Pass these token settings to PaymentService if it supports them, or
- Remove these settings from the configuration and rely entirely on the x402 library's defaults based on network, or
- Document that these settings are frontend-only and the backend uses network-based defaults
The current approach could confuse users who configure custom token settings expecting them to work end-to-end.
| JSON with network, token, and chain configuration. | |
| """ | |
| return { | |
| "network": settings.network, | |
| "tokenAddress": settings.token_address, | |
| "tokenName": settings.token_name, | |
| "tokenSymbol": settings.token_symbol, | |
| "tokenDecimals": settings.token_decimals, | |
| JSON with network and chain configuration. | |
| """ | |
| return { | |
| "network": settings.network, |
| // Load server configuration | ||
| async function loadConfig() { | ||
| try { | ||
| const response = await fetch('/config'); | ||
| if (response.ok) { | ||
| serverConfig = await response.json(); | ||
| console.log('Server config loaded:', serverConfig); | ||
| } else { | ||
| console.error('Failed to load config:', response.status); | ||
| } | ||
| } catch (error) { | ||
| console.error('Error loading config:', error); | ||
| } | ||
| } |
There was a problem hiding this comment.
The frontend loads serverConfig which includes tokenAddress, tokenName, tokenSymbol, and tokenDecimals, but never uses these values. The actual token information comes from the payment requirements (req.asset for address, req.extra?.name for name, hardcoded '2' for version).
This means the token configuration settings are loaded but unused in the payment flow. Either:
- Remove these unused fields from the /config endpoint if they're not needed by the frontend
- Use these values as defaults/fallbacks in the payment flow
- Validate that the payment requirements match the expected token configuration
Currently, users might configure custom token settings but the frontend will use whatever the backend's x402 library returns in the payment requirements.
| console.log('Payment payload:', paymentPayload); | ||
| console.log('X-Payment header length:', paymentHeader.length); | ||
| console.log('X-Payment header (first 100 chars):', paymentHeader.substring(0, 100)); | ||
| console.log('Submitting to URL:', paymentUrl); | ||
|
|
||
| showResult('pay-result', 'info', 'Submitting payment...'); | ||
|
|
||
| // Step 3: Submit payment | ||
| // Step 3: Submit payment with X-Payment header | ||
| console.log('About to fetch with X-Payment header...'); | ||
| const payResponse = await fetch(paymentUrl, { | ||
| method: 'GET', | ||
| headers: { | ||
| 'X-Payment': paymentHeader | ||
| 'X-Payment': paymentHeader, | ||
| 'Accept': 'application/json' | ||
| } | ||
| }); | ||
|
|
||
| console.log('Payment response status:', payResponse.status); | ||
| console.log('Payment response headers:', [...payResponse.headers.entries()]); | ||
|
|
||
| const payResult = await payResponse.json(); | ||
| console.log('Payment result:', payResult); |
There was a problem hiding this comment.
Multiple console.log statements have been added throughout the payment flow (lines 219, 244, 354-357, 362, 371-372, 375). While these can be useful for debugging, they may be too verbose for production use and could expose sensitive payment information in browser console logs.
Consider either:
- Wrapping these in a debug flag that can be toggled (e.g., if (DEBUG_MODE) console.log(...))
- Using a logging library that supports log levels
- Removing or reducing the verbosity of these logs for production
At minimum, avoid logging sensitive data like payment headers and signatures.
| # Patch httpx timeout - the x402 library doesn't set a timeout for settle(), | ||
| # but blockchain transactions can take longer than the default 5 seconds | ||
| import httpx | ||
|
|
||
| original_init = httpx.AsyncClient.__init__ | ||
|
|
||
| def patched_init(self: httpx.AsyncClient, *args: object, **kwargs: object) -> None: | ||
| if "timeout" not in kwargs: | ||
| kwargs["timeout"] = 60.0 # 60 seconds for blockchain transactions | ||
| original_init(self, *args, **kwargs) | ||
|
|
||
| httpx.AsyncClient.__init__ = patched_init # type: ignore[method-assign] |
There was a problem hiding this comment.
The monkey patching approach to modify httpx.AsyncClient.init is fragile and could cause issues in concurrent requests or if the library changes. This modifies a global class method that could affect other parts of the application.
Consider one of these safer alternatives:
- Create a wrapper class that extends httpx.AsyncClient with the desired timeout
- Pass the timeout configuration to the x402 library if it supports it
- Use a context manager to temporarily patch and restore the method
- Submit a PR to the x402 library to support configurable timeouts
If monkey patching is necessary, at least use a thread-local or request-scoped approach to avoid affecting concurrent requests.
| @app.get("/create") | ||
| async def create_page() -> Response: | ||
| """Serve the create payment link page.""" | ||
| create_path = STATIC_DIR / "create-payment-link.html" | ||
| if create_path.exists(): | ||
| return FileResponse(create_path) | ||
| return JSONResponse( | ||
| status_code=404, | ||
| content={"error": "Page not found"}, | ||
| ) |
There was a problem hiding this comment.
The new /create endpoint (which serves the create-payment-link.html page) has no test coverage. While the /create-payment-link API endpoint has tests, this new UI endpoint should also be tested to ensure it returns the expected HTML page.
Add a test similar to test_root_endpoint to verify that the /create endpoint returns a 200 status and serves the create-payment-link.html page.
| if "x-payment" in headers_dict and "X-Payment" not in headers_dict: | ||
| headers_dict["X-Payment"] = headers_dict["x-payment"] | ||
|
|
There was a problem hiding this comment.
The header normalization only handles the lowercase 'x-payment' to 'X-Payment' conversion. However, HTTP headers are case-insensitive, and various frameworks/proxies might normalize them differently (e.g., 'X-payment', 'x-Payment', etc.).
Consider using a case-insensitive header lookup instead. For example:
- Iterate through headers_dict to find any case variation of 'x-payment'
- Or normalize all header keys to lowercase and adjust the x402 library expectations accordingly
The current implementation could miss valid X-Payment headers that have different casing.
| if "x-payment" in headers_dict and "X-Payment" not in headers_dict: | |
| headers_dict["X-Payment"] = headers_dict["x-payment"] | |
| # Perform a case-insensitive lookup for any variant of "x-payment" | |
| payment_header_value = None | |
| for header_name, header_value in headers_dict.items(): | |
| if header_name.lower() == "x-payment": | |
| payment_header_value = header_value | |
| break | |
| # Ensure the canonical header key is present for the x402 library | |
| if payment_header_value is not None and "X-Payment" not in headers_dict: | |
| headers_dict["X-Payment"] = payment_header_value |
| showResult('success', | ||
| `Payment link created!<br><br>` + | ||
| `<strong>Payment ID:</strong> ${paymentId}<br>` + | ||
| `<strong>Amount:</strong> $${data.amount} USD<br>` + | ||
| `<strong>Receiver:</strong> ${data.receiver}` | ||
| ); |
There was a problem hiding this comment.
The showResult function uses innerHTML to display messages that include user-controlled data (data.amount and data.receiver from the API response, which echoes back user input). This could potentially lead to XSS if the backend doesn't properly sanitize these values.
While FastAPI/Pydantic provide some basic validation, it's safer to use textContent for user-provided data or properly escape HTML. For example:
- Use textContent instead of innerHTML for displaying dynamic data
- Or use a dedicated HTML escaping function before inserting into innerHTML
- Or construct DOM elements programmatically rather than using string concatenation with innerHTML
This same issue exists in the removed code from index.html (which is now fixed in the new create-payment-link.html version), so it's good that code was removed.
| # Token settings | ||
| self.token_address: str = os.getenv( | ||
| "TOKEN_ADDRESS", "0x036CbD53842c5426634e7929541eC2318f3dCF7e" | ||
| ) | ||
| self.token_name: str = os.getenv("TOKEN_NAME", "USD Coin") | ||
| self.token_symbol: str = os.getenv("TOKEN_SYMBOL", "USDC") | ||
| self.token_decimals: int = int(os.getenv("TOKEN_DECIMALS", "6")) |
There was a problem hiding this comment.
The TOKEN_VERSION configuration is added to config.py and the .env examples but is never used in the codebase. The frontend retrieves token name and version from the payment requirements (req.extra?.name and req.extra?.version) at lines 295-296 in index.html, but the server configuration doesn't include tokenVersion in the /config endpoint response.
If TOKEN_VERSION is intended to be used, it should be:
- Added to the /config endpoint response (line 92-100)
- Used in the frontend instead of falling back to the hardcoded '2'
Otherwise, remove TOKEN_VERSION from the configuration to avoid confusion.
Summary
TOKEN_ADDRESS,TOKEN_NAME,TOKEN_SYMBOL,TOKEN_DECIMALS,TOKEN_VERSIONCHAIN_ID,EXPLORER_URL/configendpoint to expose settings to the frontendindex.htmlto fetch config from server instead of using hardcoded values/configendpointNew .env Settings
For Base Mainnet
🤖 Generated with Claude Code