GET /api/security/address-check fails open: unconfigured deployments return a fabricated risk score of 50
Labels / Complexity: bug · security · Medium Complexity — Medium
Problem
src/app/api/security/address-check/route.ts proxies to Chainalysis when CHAINALYSIS_API_KEY is set — but when the key is unset, it fails open with a fabricated score:
if (!apiKey) {
return NextResponse.json({
address,
risk_score: 50,
categories: ['unknown'],
description: 'Risk check unavailable (service not configured)',
});
}
Any unconfigured deployment returns risk_score: 50 for every address — a value the UI can display as a real risk signal (the 50 is not "unknown", it is a specific number, and downstream code may threshold on it). The failure mode is "everyone gets a medium-risk score", not "risk checks are unavailable". This is the API-level counterpart of the simulated checks in src/utils/security/ (tracked separately): both present fabricated numbers as real screening.
Root cause
src/app/api/security/address-check/route.ts (the if (!apiKey) branch): returning a concrete score instead of failing closed with an explicit "unavailable" state.
Why this is architecturally hard
- The response contract must encode "unavailable". Clients must be able to distinguish "checked and clean", "checked and risky", and "not configured" — the current response collapses the last two into a number. The fix changes the API's response shape and the UI consumers (
useSecurity/WalletAddressInput), so the contract decision comes first.
- The route is also an unauthenticated quota proxy. Like
/api/simulate, it forwards to Chainalysis with no auth/rate limit; the fix should decide the gate at the same time so the endpoint is both honest and protected.
Acceptance criteria
- When
CHAINALYSIS_API_KEY is unset, the route returns an explicit "unavailable/unconfigured" state (e.g. risk_score: null + a status field) instead of the fabricated 50; clients render "not verified" rather than a numeric risk.
- A test covers both configured and unconfigured paths, asserting the response contract.
- The UI consumer renders the unavailable state distinctly.
npm run typecheck, npm test, and npm run lint pass.
Out of scope
The simulated checks in src/utils/security/ (tracked separately) and Chainalysis account setup.
Getting started
src/app/api/security/address-check/route.ts — the fail-open branch
src/hooks/useSecurity.ts, src/components/security/WalletAddressInput.tsx — the consumers of the score
Commands: npm run typecheck, npm test, npm run lint.
Good first files to read: src/app/api/security/address-check/route.ts, src/hooks/useSecurity.ts.
GET /api/security/address-check fails open: unconfigured deployments return a fabricated risk score of 50
Labels / Complexity: bug · security · Medium Complexity — Medium
Problem
src/app/api/security/address-check/route.tsproxies to Chainalysis whenCHAINALYSIS_API_KEYis set — but when the key is unset, it fails open with a fabricated score:Any unconfigured deployment returns
risk_score: 50for every address — a value the UI can display as a real risk signal (the50is not "unknown", it is a specific number, and downstream code may threshold on it). The failure mode is "everyone gets a medium-risk score", not "risk checks are unavailable". This is the API-level counterpart of the simulated checks insrc/utils/security/(tracked separately): both present fabricated numbers as real screening.Root cause
src/app/api/security/address-check/route.ts(theif (!apiKey)branch): returning a concrete score instead of failing closed with an explicit "unavailable" state.Why this is architecturally hard
useSecurity/WalletAddressInput), so the contract decision comes first./api/simulate, it forwards to Chainalysis with no auth/rate limit; the fix should decide the gate at the same time so the endpoint is both honest and protected.Acceptance criteria
CHAINALYSIS_API_KEYis unset, the route returns an explicit "unavailable/unconfigured" state (e.g.risk_score: null+ a status field) instead of the fabricated50; clients render "not verified" rather than a numeric risk.npm run typecheck,npm test, andnpm run lintpass.Out of scope
The simulated checks in
src/utils/security/(tracked separately) and Chainalysis account setup.Getting started
src/app/api/security/address-check/route.ts— the fail-open branchsrc/hooks/useSecurity.ts,src/components/security/WalletAddressInput.tsx— the consumers of the scoreCommands:
npm run typecheck,npm test,npm run lint.Good first files to read:
src/app/api/security/address-check/route.ts,src/hooks/useSecurity.ts.