Conversation
… outside dev Closes #112. Two payment middleware sites silently skipped verification when X402_SERVER_ADDRESS was missing, intended as a local-dev convenience: - src/middleware/x402.ts:292 (per-route middleware) - src/index.ts:305 (global middleware) If X402_SERVER_ADDRESS were ever unset in staging or production (bad deploy, secret rotation glitch, env-var migration), all paid endpoints would silently become free with only a warn log. Restrict the skip to ENVIRONMENT === "development". In staging and production, missing config now returns HTTP 503 with code "NOT_CONFIGURED" instead of fail-opening to free. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
x402-api-staging | 6fd4315 | Apr 30 2026, 11:58 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
x402-api-production | 6fd4315 | Apr 30 2026, 11:58 AM |
There was a problem hiding this comment.
Pull request overview
This PR addresses audit finding #112 by changing x402 payment middleware behavior to fail closed when X402_SERVER_ADDRESS is missing outside local development, preventing paid endpoints from silently becoming free due to misconfiguration.
Changes:
- Gate the “skip payment verification when
X402_SERVER_ADDRESSis missing” behavior toENVIRONMENT === "development"only. - In non-development environments, log at
errorlevel and return HTTP 503 with{ error: "x402 misconfiguration", code: "NOT_CONFIGURED" }. - Apply the same fail-closed guard in both the per-route x402 middleware and the global request middleware.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/middleware/x402.ts | Updates per-route x402 middleware to skip only in development; otherwise returns 503 on missing server address. |
| src/index.ts | Updates global middleware to skip only in development; otherwise returns 503 on missing server address before applying paid-route middleware. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Check if x402 is configured. Skip is allowed only in local development — | ||
| // staging/production deploys must have X402_SERVER_ADDRESS or all paid routes | ||
| // silently become free. | ||
| if (!c.env.X402_SERVER_ADDRESS) { | ||
| log.warn("X402_SERVER_ADDRESS not configured, skipping payment verification"); | ||
| return next(); | ||
| if (c.env.ENVIRONMENT === "development") { | ||
| log.warn("X402_SERVER_ADDRESS not configured (local dev), skipping payment verification"); | ||
| return next(); | ||
| } | ||
| log.error("X402_SERVER_ADDRESS not configured outside local dev — refusing request", { | ||
| environment: c.env.ENVIRONMENT, | ||
| }); | ||
| return c.json( | ||
| { error: "x402 misconfiguration", code: "NOT_CONFIGURED" }, | ||
| 503 | ||
| ); |
| // Skip is allowed only in local development — staging/production deploys must | ||
| // have X402_SERVER_ADDRESS or all paid routes silently become free. | ||
| if (!c.env.X402_SERVER_ADDRESS) { | ||
| c.var.logger.warn("X402_SERVER_ADDRESS not configured, skipping payment verification"); | ||
| return next(); | ||
| if (c.env.ENVIRONMENT === "development") { | ||
| c.var.logger.warn("X402_SERVER_ADDRESS not configured (local dev), skipping payment verification"); | ||
| return next(); | ||
| } | ||
| c.var.logger.error("X402_SERVER_ADDRESS not configured outside local dev — refusing request", { | ||
| environment: c.env.ENVIRONMENT, | ||
| }); | ||
| return c.json( | ||
| { error: "x402 misconfiguration", code: "NOT_CONFIGURED" }, | ||
| 503 | ||
| ); |
| // Skip is allowed only in local development — staging/production deploys must | ||
| // have X402_SERVER_ADDRESS or all paid routes silently become free. | ||
| if (!c.env.X402_SERVER_ADDRESS) { | ||
| c.var.logger.warn("X402_SERVER_ADDRESS not configured, skipping payment verification"); | ||
| return next(); | ||
| if (c.env.ENVIRONMENT === "development") { | ||
| c.var.logger.warn("X402_SERVER_ADDRESS not configured (local dev), skipping payment verification"); | ||
| return next(); | ||
| } | ||
| c.var.logger.error("X402_SERVER_ADDRESS not configured outside local dev — refusing request", { | ||
| environment: c.env.ENVIRONMENT, | ||
| }); | ||
| return c.json( | ||
| { error: "x402 misconfiguration", code: "NOT_CONFIGURED" }, | ||
| 503 | ||
| ); |
arc0btc
left a comment
There was a problem hiding this comment.
Closes a real security gap — payment routes silently becoming free on any config drift is exactly the kind of thing that's invisible until it's a problem. Good catch and clean fix.
What works well:
- Fail-closed is the right default. A 503 is far better than silent free access in staging/production.
- Using
ENVIRONMENT === "development"is the right choice —X402_NETWORKwould have required type widening, andENVIRONMENTis already set to"development"in local wrangler config. The PR body explains this clearly, which I appreciate. - Logging at
errorlevel in misconfigured non-dev environments is correct — this is a condition that needs to page someone, not just be noticed in logs. - The
code: "NOT_CONFIGURED"field in the 503 body is a nice touch for operators debugging the issue.
[suggestion] Extract the shared guard into a helper (src/middleware/x402.ts + src/index.ts)
The 11-line misconfiguration guard block is duplicated exactly in both files. If the behavior needs to change again (e.g., different HTTP status, additional context in the error body), it'll need to change in two places. Not blocking — the duplication here is small and the fix is more important than the cleanup. Worth a follow-up once this is merged.
[nit] The 503 response doesn't include a Retry-After header. For a misconfiguration error, retry won't help — so this is fine as-is. Just noting it in case the team wants to add an ops hint for callers.
Operational note: We've seen x402 env var issues operationally when rotating secrets or updating wrangler configs mid-deploy. Having the middleware fail with a loud 503 instead of silently passing requests through is exactly what we'd want — makes the misconfiguration immediately visible in monitoring rather than only discoverable by checking revenue.
Test plan looks right. The staging-without-X402_SERVER_ADDRESS case is the critical one to verify before merge.
Summary
Closes #112 (fail-open audit findings).
Two payment middleware sites silently skipped payment verification when
X402_SERVER_ADDRESSwas missing, intended as a local-dev convenience but failing open in any environment:src/middleware/x402.ts:292(per-route x402 middleware)src/index.ts:305(global request middleware)If
X402_SERVER_ADDRESSwere ever unset in staging or production (bad deploy, secret rotation glitch, env-var migration, env-var name typo), every paid endpoint would silently become free with only awarnlog.Fix (Option A from the audit)
Restrict the skip to
ENVIRONMENT === \"development\". In staging and production:errorlevel (notwarn){ error: \"x402 misconfiguration\", code: \"NOT_CONFIGURED\" }ENVIRONMENTis already plumbed through wrangler.jsonc per env (development/staging/production).Why ENVIRONMENT and not X402_NETWORK
X402_NETWORKis typed strictly as\"mainnet\" | \"testnet\"insrc/types.ts:53— using a\"local\"literal would require widening that type.ENVIRONMENTis already declared asstring(line 50) and already takes thedevelopmentvalue in local dev (wrangler.jsonc:35). Cleaner.Test plan
npm run checkpassesENVIRONMENT=development, no X402_SERVER_ADDRESS) — skip behavior preserved🤖 Generated with Claude Code