Files
mortgagefi-helper/docs/Audits/Project Audit 2026-06.md
Siavash Sameni 6ae581ab2e feat(ui): Ghibli/Miyazaki reskin + Obsidian docs vault + project audit
UI: warm daylight design system (Tailwind v4 @theme palette, gh-* component
classes, watercolor grain, Zen Maru Gothic + Klee One fonts), animated SSR-safe
GhibliBackground (drifting clouds, meadow hills, soot sprites), and a full reskin
of navbar, connect button, dapp page, loan cards, settings modal, and readme.
Fixes the bg-white-on-dark loan-card inconsistency. Web3/business logic untouched.

Docs: converted docs/ into an Obsidian vault (frontmatter, [[wikilinks]],
callouts, Home MOC, folders Architecture/Operations/Audits) and added a
full-project audit note (Project Audit 2026-06). Redacted a real leaked Schedy
key value from the security audit example (rotate it at Schedy).

Also commits the previously-untracked server layer: app/api (cron + tasks routes)
and lib (redis, ssrf-guard, task-store).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 08:13:53 +04:00

26 KiB

title, tags, type, status, updated
title tags type status updated
Project Audit 2026-06
mortgagefi
audit
security
performance
dependencies
audit current 2026-06-14

Project Audit — June 2026

A fresh, whole-project review of MortgageFi after a period of inactivity, covering the frontend web3 logic, the server/API layer, the Go nftcache service, infrastructure/ops, and dependencies. It complements the deeper, topic-specific Security Audit and Performance Audit — where those go further on a topic, they are linked inline.

[!danger] Three issues need attention before anything else

  1. A live secret is shipped to every browserNEXT_PUBLIC_SCHEDY_API_KEY is set in .env, so it is inlined into the client bundle. Rotate the Schedy key now and remove the NEXT_PUBLIC_ variant.
  2. The scheduler's SSRF guard is bypassable — it validates the hostname string at creation time but the cron worker fires the request later with no re-validation, no redirect control, and no DNS-rebinding protection.
  3. GET /api/tasks and DELETE /api/tasks/[id] have no authentication — anyone can enumerate every scheduled task (and the secrets stored in their headers) or delete them.

Executive summary

The codebase is, structurally, in good shape: a modern and internally-consistent core stack (Next.js 16, React 19, wagmi v3 + viem v2, TypeScript strict), a clean scheduling migration to Next.js API routes + Upstash Redis, non-root Docker images that aren't port-published, and genuine security thought (RPC allowlist, known-preset compound guard, SSRF allowlist scaffolding, a real CSP).

The problems are concentrated in secret handling, the outbound-request (SSRF) trust model, endpoint authentication, and the NFT-scanning design (both the frontend ownerOf walk and the backend serial scan are correctness-fragile and DoS-prone). There is also notable dead weight: two unused web3 stacks (@web3-react, @web3modal/wagmi) and the orphaned schedy submodule that Docker still builds and runs.

Findings by severity

Domain 🔴 Critical 🟠 High 🟡 Medium 🟢 Low Info
Frontend web3 0 3 7 4 1
API & server libs 2 3 3 2 1
nftcache (Go) 2 4 4 3 2
Infra & ops 2 3 3 2 2
Dependencies & build 0 2 2 2 1
Total 6 15 19 13 7

[!note] Severity is engineering judgement, not a formal CVSS score. The two "Critical" deps/web3modal-style items were normalised down because the affected code is unused; see each finding.

Priority remediation roadmap

[!todo] Do first (Critical)

  • Rotate the Schedy API key and delete NEXT_PUBLIC_SCHEDY_API_KEY; proxy Schedy through a server-side route that reads the non-public SCHEDY_API_KEY. Remove the stray manwe-secret line.
  • Rewrite lib/ssrf-guard.ts to validate at fire-time: resolve all A/AAAA records, reject any private/reserved/loopback/link-local IP, pin the connection to the validated IP (defeat rebinding), set redirect: 'manual' and re-validate each hop, and parse decimal/octal/hex/IPv4-mapped-IPv6 forms with a real IP library.
  • Authenticate GET /api/tasks and DELETE /api/tasks/[id], scope tasks to an owner, and never return raw headers/payload.
  • nftcache: move the API-key check to the top of every handler (before any param parsing, cache access, or RPC), and restrict nft_contract/network to the YAML allowlist so an arbitrary address can't trigger a 10k-call scan.
  • Audit published images for baked-in secrets (COPY . . + missing frontend .dockerignore); add a .dockerignore excluding .env*, then rotate anything that may have shipped.

[!todo] Do next (High)

  • Make CRON_SECRET (frontend) and X-API-Key (nftcache) comparisons constant-time (crypto.timingSafeEqual / crypto/subtle).
  • Fail the stack closed when REDIS_REST_TOKEN is unset; stop shipping local-redis-token-change-me as a working default.
  • Make the rate limiter atomic + fail-closed, and stop trusting client X-Forwarded-For for the key.
  • Replace the frontend's sequential ownerOf gap-scan with Transfer-log / enumerable / multicall detection; only count a "gap" on a true nonexistent token revert, and never permanently mark a wallet "complete" after transient errors.
  • Give nftcache HTTP-server timeouts + graceful shutdown (cache.Close()), thread context through RPC calls, and add single-flight to the stale-refresh path.
  • Add nginx limit_req / client_max_body_size; confirm ntfy isn't open (NTFY_AUTH_DEFAULT_ACCESS=deny-all).
  • Gate write actions (pay/approve/compound) on chainId === selectedChainId; scope the compound preset check to the selected chain.
  • Remove @web3-react/* and @web3modal/wagmi; remove the orphaned schedy service from docker-compose.yml/Dockerfile.

1. Frontend web3 & UI logic

Files: app/dapp/page.tsx, config/web3.ts, providers/Web3Provider.tsx, utils/scheduler.ts, utils/useLocalStorage.ts, components/SettingsModal.tsx

The web3 read plumbing is mostly sound (on-chain decimals/symbol, parseUnits/BigInt money math, payment capped to currentPaymentPending). The weak spots are the NFT-discovery heuristics and several correctness traps around chain switching.

🟠 High

  • Gap-limit scan conflates RPC errors with non-existent tokensapp/dapp/page.tsx:339-392. Any non-429 error is treated as a "definitive gap"; 5 in a row write complete: true, after which scanMore early-returns forever and the wallet looks empty — a safety-relevant false negative that can hide a position nearing liquidation. Fix: only count a gap on an actual nonexistent token revert; persist a "stopped due to errors" state.
  • Sequential scan assumes contiguous tokenIdsapp/dapp/page.tsx:325-371. Walks 0,1,2,… and stops after 5 misses; burns/non-sequential IDs are undetectable except by manual entry. Fix: prefer Transfer-log scan or ERC721Enumerable (tokenOfOwnerByIndex) — see the dead-code finding below.
  • Chain-switch raceapp/dapp/page.tsx:429-437. selectedChainId drives every read while the wallet may be on another chain; the auto-switchChain result is ignored and reads/writes proceed regardless. Fix: gate writes on chainId === selectedChainId, await switchChainAsync, prompt the user instead of silent auto-switch.

🟡 Medium

  • APR display is a magnitude heuristic:991-1003. Infers scale at runtime (>1e9 ⇒ 1e18 scale, else basis points); any other denominator is mis-rendered, potentially off by orders of magnitude for a financial figure. Fix: read/hardcode the real per-preset denominator; unit-test against on-chain value.
  • Loan-term/expiration heuristic:1226-1237. expiration <= 200 ? "N Years" : date. Conflates two meanings of one field by magnitude. Fix: format deterministically from the ABI's actual semantics.
  • Provider tokens & API keys in localStorage, logged to console:116-118, :1404; SettingsModal.tsx:330-334; useLocalStorage.ts. Gotify token, ntfy topic, emails, NFTCache API key persisted and the whole settings object repeatedly console.log'd. Fix: strip the logging; treat the tokens as secrets (don't persist, or schedule server-side).
  • nftcacheApiKey saved but no UI to view/clear itSettingsModal.tsx:36,51-56. Half-wired secret that can be persisted but never edited or removed via the UI. Fix: add the field or remove the state.
  • Custom-RPC trust model is partial & silentconfig/web3.ts:11-49. The allowlist only governs read transports (not the wallet's signing RPC), the Alchemy regex accepts any key-shaped suffix, and a rejected RPC silently falls back to a public default with only a console.warn. Fix: surface rejection in the UI; document the scope.
  • Monolithic component re-renders + un-debounced auto-reschedule — whole file; effects at :250-304, :929-955. ~25 hooks in one component; every keystroke re-renders the tree, and the auto-reschedule effect fires scheduleJob/cancel on >60s drift with exhaustive-deps disabled — risking duplicate/cancelled jobs from stale closures. Fix: extract child components/hooks, debounce, add an in-flight guard. See Performance Audit for the broader re-render analysis.
  • Compound guard checks address but not chain:1017-1039. Validates debtAddress across all chains' presets, then writes with selectedChainId. Fix: scope to PRESETS[selectedChainId] and require chain match.

🟢 Low

  • Dead enumerable code path:481-502. tokenOfOwnerByIndex reads are built then discarded; the more-correct enumeration is never used. Fix: implement via multicall or delete.
  • parseUnits failures swallowed in pay/approve handlers:697-736. Bad input ("1.2.3", "1e5") throws and is only console.warn'd; the money action silently no-ops. Fix: surface parse errors; reuse the render's amount === null guard.
  • Scheduler fabricates a job id on ambiguous responsesutils/scheduler.ts:45-65. A non-JSON/id-less response yields task_<random>, so a failed schedule looks successful (false confidence an alert exists). Fix: treat missing id as failure.
  • useLocalStorage trusts persisted JSON with no schema validationutils/useLocalStorage.ts:18-39. Tampered/corrupt storage is accepted as typed settings that drive network requests; double-effect can cause a redundant write. Fix: validate shape (zod) before accepting.

Info

  • Deep-link payload from localStorage drives chain/preset:170-195. Same-origin, age-bounded, and IDs are later guarded — acceptable; consider range-validating preset/tokenId.

2. API & server libs

Files: app/api/cron/route.ts, app/api/tasks/route.ts, app/api/tasks/[id]/route.ts, lib/redis.ts, lib/ssrf-guard.ts, lib/task-store.ts, utils/cronhost.ts

The skeleton is sound (CRON_SECRET gate, an SSRF allowlist, a rate limiter, header sanitisation) but the SSRF guard doesn't actually protect the outbound request, and two endpoints are unauthenticated.

🔴 Critical

  • SSRF guard bypassable via DNS rebinding, redirects, and IP encodingslib/ssrf-guard.ts:30-84; enforcement gap at app/api/cron/route.ts:36. Validation runs on the literal hostname at creation time only. Bypasses: (1) DNS rebinding — attacker.com passes, then resolves to 169.254.169.254/127.0.0.1 at fire time; (2) no redirect: 'manual', so a whitelisted host can 302 to metadata; (3) decimal/octal/hex IPv4 (http://2130706433/, 0177.0.0.1, 127.1) skip the dotted-quad regex; (4) IPv4-mapped IPv6 ([::ffff:169.254.169.254]) and long-form loopback bypass the prefix checks; (5) GCP metadata.google.internal and the decimal form of the IMDS address aren't blocked. Fix: see the roadmap — resolve-and-pin at fire time. The BLOCKED_HOSTS Docker-service-name list confirms an internal network is reachable. (Also tracked in Security Audit.)
  • No auth on GET /api/tasks & DELETE /api/tasks/[id]app/api/tasks/route.ts:65-72; app/api/tasks/[id]/route.ts:4-19. GET returns every task including each url, full headers (which legitimately carry Authorization/API keys for the target) and payload; DELETE removes any task whose id startsWith('task_'). Anonymous credential disclosure + sabotage/DoS, with no per-owner scoping anywhere. Fix: authenticate + scope + redact.

🟠 High

  • Stored task headers carry secrets and are returned/forwarded unredactedapp/api/tasks/route.ts:50,59; lib/task-store.ts:21. sanitizeHeaders strips only hop-by-hop headers; Authorization/Cookie/X-Api-Key survive, are stored in plaintext, echoed in the POST 201, exposed by the unauthenticated GET, and replayed on every retry. Fix: encrypt at rest, never echo, redact on cross-origin redirects.
  • Rate limiter fails open + check-then-act race + spoofable keylib/task-store.ts:76-91; app/api/tasks/route.ts:6-12. Returns allow when Redis is unconfigured; non-atomic count under concurrency; keyed off client-controlled x-forwarded-for; collapses missing-IP callers to one unknown bucket. Fix: atomic sliding window (Lua/INCR+EXPIRE), fail closed, trusted-proxy IP only.
  • CRON_SECRET comparison not timing-safeapp/api/cron/route.ts:9-13. Plain !== on the one credential protecting the highest-privilege endpoint. Fix: crypto.timingSafeEqual over equal-length buffers.

🟡 Medium

  • listAllTasks uses KEYS + N+1 GETlib/task-store.ts:58-73 (and listDueTasks N+1 at :51-54). KEYS is O(N)/blocking and Upstash-discouraged; an attacker can trigger it repeatedly via the open GET. Fix: drive listing off the existing TASK_ZSET with ZRANGE + MGET.
  • Cron forces POST + overrides stored content-type inconsistentlyapp/api/cron/route.ts:37,40. Tasks store no method; the worker always POSTs and hardcodes content-type while letting other arbitrary headers through. Fix: make method/content-type explicit and validated; consistent precedence.
  • Weak input validation (URL length, payload size, header values)app/api/tasks/route.ts:21-26,50. No max URL length, no payload-size cap (stored & sent verbatim), header values not type/CRLF-checked. Fix: enforce limits; reject control/CRLF chars.

🟢 Low

  • Internal error detail leaked to clientscron/route.ts:20, tasks/route.ts:34,61,70. Blocked URL: ${reason} even discloses which SSRF rule fired. Fix: generic client errors, detailed server logs.
  • Contradictory localhost policy in the guardlib/ssrf-guard.ts:39-48. Dev carve-out for http://localhost is immediately undone by BLOCKED_HOSTS; signals the guard wasn't tested end-to-end. Fix: resolve the contradiction; key protocol policy off an explicit flag, not NODE_ENV.

Info

  • utils/cronhost.ts is dead/legacy stub code — confirmed no imports anywhere. Fix: delete it.

3. nftcache (Go service)

Files: cmd/nftcache/main.go, internal/config/config.go, internal/fetcher/rpc.go, internal/fetcher/alchemy.go, internal/store/store.go, go.mod

A BadgerDB-backed stale-while-revalidate cache fronting per-token ownerOf scans. Auth and address validation exist but the authorization ordering is fragile and the scan design is a DoS amplifier.

🔴 Critical

  • API-key check ordering is fragilecmd/nftcache/main.go:82-124,239-245. The key is checked after param/config resolution; gating currently works but any future edit touching RPC before line 122 would expose an unauthenticated amplifier. Fix: check the key first thing in every handler (middleware).
  • Serial RPC scan is a DoS amplifierinternal/fetcher/rpc.go:55-113, called from main.go:172,245. One request → up to maxTokenId (cap 10000) serial eth_calls against the upstream, throttled by a global 5 TPS limiter, for any caller-supplied network+nft_contract. 1 HTTP request → up to 10k upstream calls, and one abusive scan starves all legitimate traffic. Fix: reject contracts not in the YAML allowlist; bound per-request concurrency; per-call deadlines.

🟠 High

  • Non-constant-time API-key comparemain.go:123,239,262. Fix: crypto/subtle.ConstantTimeCompare.
  • No HTTP-server timeouts & no graceful shutdownmain.go:303, main(). Default server (no Read/Write/Idle timeouts → slowloris), no SIGTERM handler, cache.Close() never called (Badger not flushed). Fix: explicit http.Server timeouts + server.Shutdown + defer cache.Close().
  • No context/timeout on RPC calls; scans uncancellablerpc.go:23,227; FetchAllTokenOwners takes no context. Hung upstream blocks a scan goroutine indefinitely; client disconnect doesn't abort a 10k-call scan; background refreshes are fire-and-forget. Fix: http.Client{Timeout}, thread context, derive refresh ctx from server lifetime.
  • Stale-refresh thundering herd (no single-flight)main.go:143-162. N concurrent stale requests launch N full scans of the same contract. Fix: singleflight per contract key.

🟡 Medium

  • Serial scanning instead of batch/multicallrpc.go:65-109. 1000 tokens ≥200s, 10000 ≥33min. Multicall3 (same address on eth/arb/base) or JSON-RPC batch, ERC721Enumerable, or the already-implemented-but-unused Alchemy getNFTsForOwner would collapse this. Fix: adopt multicall/Alchemy path. (See Performance Audit.)
  • Upstream/internal error detail leaked to clientsmain.go:182,246,267. Fix: generic messages, server-side logs.
  • Rate-limit handling deletes cache → feedback loopmain.go:151-154,177-180. On 429 exhaustion it deletes the entry, so the next request rescans into the already-limited upstream; detection is brittle substring matching. Fix: keep stale data + back off; typed/sentinel errors.
  • No network allowlist; CORS supports *main.go:83,38-52. Bad networks fail deep in the scan; CORS_ALLOW_ORIGIN=* is allowed (default empty is safe). Fix: validate network up front; document * must not pair with credentialed flows.

🟢 Low

  • Leftover "Token 103" debug + unconditional fmt.Printfrpc.go:106-111, main.go:197-199. Fix: remove special-cases; gate behind a debug flag; use log consistently.
  • canonAddr accepts any 40-hex string; duplicated in two filesmain.go:64-80, rpc.go:141-157. Fix: combine with the allowlist; dedupe the helper.
  • json.Marshal errors ignored in store writesinternal/store/store.go:50,58. Fix: check & propagate.

Info

  • No Badger-layer TTL / value-log GCstore/store.go:49-66. Entries never expire on disk (app-side SWR only) → unbounded growth. Fix: optional Badger TTL + periodic RunValueLogGC.
  • Rate limiter is approximate (burst >5 TPS) + refill goroutine leaks on exitrpc.go:24-43. Acceptable for a soft limiter; use x/time/rate if strictness is needed.

Stale dependencies

go 1.22.0 (current 1.24+), badger/v4 v4.2.0, and several stale indirect deps with advisories — notably golang.org/x/net v0.7.0 (HTTP/2 rapid-reset family), google.golang.org/protobuf v1.28.1 (< 1.33.0 JSON unmarshal loop), golang.org/x/sys v0.5.0. Fix: go get -u ./..., bump the Go directive, run govulncheck.


4. Infrastructure & ops

Files: docker-compose.yml, nginx/nginx.conf, .env.example, .env / .env.local, config/contracts.yaml, both Dockerfiles, next.config.ts

Self-hosted Compose stack. Architecture is reasonable (internal services not port-published, non-root images, a real CSP) but secret handling and the nginx edge need work.

🔴 Critical

  • Secret shipped to the client via NEXT_PUBLIC_.env / .env.local. A real Schedy API key is set as both SCHEDY_API_KEY and NEXT_PUBLIC_SCHEDY_API_KEY; the latter is inlined into the JS every browser downloads. A stray manwe-secret duplicates the WalletConnect id. Fix: delete the NEXT_PUBLIC_ variant, proxy Schedy server-side, rotate the key, remove the stray line.
  • Live third-party secrets on disk.env, mortgagefi-frontend/.env.local. Contains real ALCHEMY_API_KEY, NFTCACHE_API_KEY, SCHEDY_API_KEY, CRONHOST_API, and a Gmail SMTP app password. Root .gitignore covers them and this isn't a git repo today, but one git init/sub-repo commit away from exposure. Fix: secret manager / Docker secrets; verify the frontend subdir's own .gitignore covers .env.local; ensure Docker build context doesn't bake them in (next finding).

🟠 High

  • Weak hardcoded default Redis tokendocker-compose.yml:16,76; .env.example:23. SRH_TOKEN/UPSTASH_REDIS_REST_TOKEN default to local-redis-token-change-me, shipped as a working value in .env.example; the stack is likely running with it. Anyone on the Docker network can then read/write all scheduling state. Fix: ${REDIS_REST_TOKEN:?set me} (fail closed); set a strong random token.
  • COPY . . may bake .env into the frontend imagemortgagefi-frontend/Dockerfile:17. No confirmed .dockerignore; the .env.local target lives inside the frontend dir, and NEXT_PUBLIC_* values inline into the bundle in the published image …/mortgagefi-frontend:alert. Fix: add .dockerignore (.env*, node_modules, .next, data/); inspect the published image; rotate anything baked in.
  • No rate limiting / body-size limit / edge auth on proxied pathsnginx/nginx.conf. Port 80 forwards /ntfy/ (full UI + publish API), /nftcache/, and the app with no limit_req, limit_conn, or client_max_body_size; security rests entirely on each service's own auth (ntfy default is open). Fix: add client_max_body_size + limit_req zones; auth_basic/IP-allowlist /ntfy/; set NTFY_AUTH_DEFAULT_ACCESS=deny-all.

🟡 Medium

  • Cron loop swallows all errors + plaintext Bearer + spurious depsdocker-compose.yml:84-102. curlimages/curl:latest loop with || true hides every failure (no alerting/dead-man's-switch); depends on redis it never uses. Fix: log non-2xx (drop || true), pin the image, drop the redis dep, confirm /api/cron 401s on bad secret and isn't reachable via nginx /.
  • Mutable / :latest image tagsdocker-compose.yml:11,23,67,85,105. serverless-redis-http:latest, untagged ntfy, mutable mortgagefi-frontend:alert, curl:latest. Fix: pin to digests/immutable tags.
  • No healthchecks on any servicedocker-compose.yml. depends_on only waits for start, not readiness → startup 502s, no auto-restart on hung process. Fix: add healthcheck + condition: service_healthy.

🟢 Low

  • Duplicate YAML key in nftcache configconfig/contracts.yaml:10-18. cbbtc defined twice (placeholder vs real); YAML silently keeps the last. Fix: remove the placeholder.
  • Security-header drift between nginx and Next.jsnginx/nginx.conf:6-9,74-79 vs next.config.ts. nginx sets no CSP; Next.js does; overlap can diverge on error pages. Fix: centralise headers in one layer (prefer the Next.js CSP).

Info

  • proxy_buffering off is globalnginx/nginx.conf:12. Needed for ntfy SSE but applied everywhere → higher upstream hold time. Fix: scope it to /ntfy/.
  • ntfy uses a personal Gmail app passworddocker-compose.yml:31-34. Fix: prefer a scoped transactional-mail credential; rotate if ever shared.

5. Dependencies & build

Files: mortgagefi-frontend/package.json, tsconfig.json, next.config.ts, eslint.config.mjs, nftcache/go.mod, submodules/schedy

Core stack is modern and coherent (Next 16, React 19, wagmi v3 + viem v2, TS strict). The issues are dead weight and a few hardening gaps.

🟠 High

  • Deprecated, unused, version-mismatched @web3modal/wagmi@^5package.json. v5 targets wagmi v2 but the project runs wagmi v3; it's also superseded by Reown AppKit — and it isn't imported anywhere. Fix: remove it; adopt @reown/appkit + @reown/appkit-adapter-wagmi only if a connect modal is later needed. (Listed High, not Critical: unused = not exploitable, but it's deprecated + mismatched.)
  • Unused redundant @web3-react/* stackpackage.json. @web3-react/core@^8, @web3-react/injected-connector@^6 are unmaintained and fully unused; the app is bare wagmi v3. Fix: remove both; use wagmi/connectors if needed.
  • Orphaned schedy submodule still built & run by Dockersubmodules/schedy/, docker-compose.yml, Dockerfile:22. Scheduling moved to Next.js API routes + Upstash; all in-source schedy references are now legacy-prune code or an SSRF-allowlist token, yet Docker still builds the Go scheduler and sets NEXT_PUBLIC_SCHEDY_URL. Fix: confirm nothing deploys it, then delete the submodule, the schedy service/depends_on, and NEXT_PUBLIC_SCHEDY_URL; clean residual comments/keys.

🟡 Medium

  • nftcache Go module on an old toolchain with stale transitive depsnftcache/go.mod. (Same set as §3.) Fix: bump Go + go get -u ./... + govulncheck.
  • CSP allows 'unsafe-eval' and 'unsafe-inline' for scriptsnext.config.ts. Meaningfully weakens XSS protection for a wallet-signing dApp. Fix: move toward nonce/hash-based script-src, drop 'unsafe-eval' if wagmi/walletconnect tolerate it; drop the deprecated X-XSS-Protection; consider adding HSTS.

🟢 Low

  • Packages a minor behind (some security-relevant)npm outdated: next 16.0.10 → 16.2.9, wagmi 3.1.0 → 3.6.16, viem 2.42.1 → 2.52.2, @tanstack/react-query, tailwindcss, framer-motion. Treat Next.js patch bumps as security-relevant. Fix: run in-range minors; evaluate eslint 10 / TS 6 majors separately.
  • Minimal ESLint configeslint.config.mjs. Only Next defaults; no no-floating-promises/no-misused-promises despite fire-and-forget await fetch patterns. Fix: add @typescript-eslint recommended-type-checked rules.

Info

  • TS target: ES2017 is datedtsconfig.json (strict is otherwise good). Fix: optionally raise to ES2022, consider noUncheckedIndexedAccess.

What's done well

  • On-chain decimals/symbol and BigInt/parseUnits math throughout — no float drift in money paths; payments capped to the reset-timer amount.
  • Genuine security scaffolding: RPC allowlist, known-preset compound guard, SSRF allowlist + header sanitisation, CRON_SECRET-gated cron that fails closed, future-dating bounds on execute_at, unguessable crypto.randomUUID() task ids.
  • nftcache: API key required at startup (fails fast if unset), CORS closed by default, correct Badger transaction usage, stale-while-revalidate for latency, 429 backoff.
  • Infra: internal services not port-published, non-root + distroless images, CGO_ENABLED=0, read-only config mount, consistent platform pinning, a real CSP + security-header baseline, correct .gitignore, output: 'standalone'.
  • Clean, self-contained scheduling migration to Next.js API routes + Upstash Redis.

Home · Architecture · Security Audit · Performance Audit · Deployment · Development