Skip to content

Make token and chain settings configurable via .env#3

Merged
juntao merged 13 commits intomainfrom
feature/configurable-token
Feb 2, 2026
Merged

Make token and chain settings configurable via .env#3
juntao merged 13 commits intomainfrom
feature/configurable-token

Conversation

@juntao
Copy link
Member

@juntao juntao commented Feb 1, 2026

Summary

  • Add configurable token settings: TOKEN_ADDRESS, TOKEN_NAME, TOKEN_SYMBOL, TOKEN_DECIMALS, TOKEN_VERSION
  • Add configurable chain settings: CHAIN_ID, EXPLORER_URL
  • Add /config endpoint to expose settings to the frontend
  • Update index.html to fetch config from server instead of using hardcoded values
  • Add test for the new /config endpoint

New .env Settings

# Token Settings (defaults are for USDC on Base Sepolia)
TOKEN_ADDRESS=0x036CbD53842c5426634e7929541eC2318f3dCF7e
TOKEN_NAME=USD Coin
TOKEN_SYMBOL=USDC
TOKEN_DECIMALS=6
TOKEN_VERSION=2

# Chain Settings (defaults are for Base Sepolia)
CHAIN_ID=84532
EXPLORER_URL=https://sepolia.basescan.org/tx/

For Base Mainnet

NETWORK=base
TOKEN_ADDRESS=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
TOKEN_NAME=USD Coin
TOKEN_SYMBOL=USDC
TOKEN_DECIMALS=6
TOKEN_VERSION=2
CHAIN_ID=8453
EXPLORER_URL=https://basescan.org/tx/

🤖 Generated with Claude Code

Juntao Yuan and others added 13 commits February 1, 2026 23:24
- 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>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /config API endpoint to expose configuration to the frontend
  • Separated payment creation UI into a new page at /create endpoint
  • Updated frontend to load server configuration instead of using hardcoded values
  • Added test coverage for the new /config endpoint

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.

Comment on lines +479 to +493
// 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);
}
}

Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Show a prominent warning to the user if config fails to load on page initialization
  2. Retry loading the config if it fails
  3. 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.

Suggested change
// 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;
}

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +42
# 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/"
)
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +97
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,
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. The frontend shows one set of token settings from /config
  2. 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.

Suggested change
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,

Copilot uses AI. Check for mistakes.
Comment on lines +479 to +492
// 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);
}
}
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Remove these unused fields from the /config endpoint if they're not needed by the frontend
  2. Use these values as defaults/fallbacks in the payment flow
  3. 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.

Copilot uses AI. Check for mistakes.
Comment on lines +354 to 375
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);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Wrapping these in a debug flag that can be toggled (e.g., if (DEBUG_MODE) console.log(...))
  2. Using a logging library that supports log levels
  3. Removing or reducing the verbosity of these logs for production

At minimum, avoid logging sensitive data like payment headers and signatures.

Copilot uses AI. Check for mistakes.
Comment on lines +252 to +263
# 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]
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Create a wrapper class that extends httpx.AsyncClient with the desired timeout
  2. Pass the timeout configuration to the x402 library if it supports it
  3. Use a context manager to temporarily patch and restore the method
  4. 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.

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +82
@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"},
)
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +197 to +199
if "x-payment" in headers_dict and "X-Payment" not in headers_dict:
headers_dict["X-Payment"] = headers_dict["x-payment"]

Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +243
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}`
);
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +36
# 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"))
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Added to the /config endpoint response (line 92-100)
  2. Used in the frontend instead of falling back to the hardcoded '2'

Otherwise, remove TOKEN_VERSION from the configuration to avoid confusion.

Copilot uses AI. Check for mistakes.
@juntao juntao merged commit 9cca9cb into main Feb 2, 2026
7 checks passed
@juntao juntao deleted the feature/configurable-token branch February 2, 2026 02:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants