Skip to content

Referral Program Cycles#1603

Open
Goader wants to merge 13 commits intomainfrom
feat/referral-program-cycles
Open

Referral Program Cycles#1603
Goader wants to merge 13 commits intomainfrom
feat/referral-program-cycles

Conversation

@Goader
Copy link
Contributor

@Goader Goader commented Feb 3, 2026

ENS Referrals API v1: Multi-Cycle Support

closes: #1522


Reviewer Focus (Read This First)

What reviewers should focus on
  • Cycle ID typing strategy - ReferralProgramCycleId type allows both predefined IDs (cycle-1, cycle-2) with autocomplete AND custom cycle IDs loaded from JSON (using string & {} trick for extensibility)
  • Configuration loading strategy in apps/ensapi/src/config/config.schema.ts - loadReferralProgramCycleSet() with URL fetching and validation

Problem & Motivation

Why this exists

What Changed (Concrete)

What actually changed

1. New Data Models (packages/ens-referrals/src/v1/)

  1. cycle.ts - Core type definitions:

    • ReferralProgramCycleIds object with Cycle1: "cycle-1" and Cycle2: "cycle-2"
    • ReferralProgramCycleId type (extensible to custom IDs)
    • ReferralProgramCycle interface with id, displayName, rules, rulesUrl
    • ReferralProgramCycleSet type as Map<ReferralProgramCycleId, ReferralProgramCycle>
  2. cycle-defaults.ts - Default cycle definitions:

    • getReferralProgramCycleSet() function returning pre-built Map with cycle-1 and cycle-2
    • Cycle 1: ENS Holiday Awards (Dec 2025)
    • Cycle 2: March 2026
    • Both with proper start/end times, rules, and rules URLs
  3. api/zod-schemas.ts - Comprehensive validation:

    • makeReferralProgramCycleIdSchema() - validates cycle IDs
    • makeReferralProgramCycleSchema() - validates complete cycle objects
    • makeCustomReferralProgramCyclesSchema() - validates JSON array format with duplicate ID checks
    • makeReferralProgramCycleSetSchema() - validates Map structure
  4. api/types.ts - New response types:

    • ReferrerDetailAllCyclesData - Record of cycle ID to referrer details
    • ReferrerDetailAllCyclesResponse - Discriminated union for OK/Error
    • Full serialization/deserialization support

2. Configuration System (apps/ensapi/)

  1. Environment variable (packages/ensnode-sdk/src/shared/config/environments.ts):

    • Added CUSTOM_REFERRAL_PROGRAM_CYCLES?: string to ReferralProgramCyclesEnvironment
    • Optional URL to JSON file with custom cycle definitions
  2. Config schema (apps/ensapi/src/config/config.schema.ts):

    • Added loadReferralProgramCycleSet() async function:
      • Validates URL format
      • Fetches JSON
      • Validates against Zod schema
      • Converts array to Map with duplicate ID checking
      • Comprehensive error messages on failure (crashes ENSApi if invalid)
    • Updated EnsApiConfigSchema to include referralProgramCycleSet field
    • Updated buildConfigFromEnvironment() to call loader
  3. Environment documentation (apps/ensapi/.env.local.example):

    • Added comprehensive documentation for CUSTOM_REFERRAL_PROGRAM_CYCLES
    • Includes example JSON structure
    • Notes about USDC amount encoding (smallest units)
    • Removed ENS_HOLIDAY_AWARDS_START and ENS_HOLIDAY_AWARDS_END (no longer needed)

3. Multi-Cycle Cache Architecture (apps/ensapi/src/cache/)

  1. referral-leaderboard-cycles.cache.ts - New cache implementation:

    • ReferralLeaderboardCyclesCacheMap type: Map<ReferralProgramCycleId, SWRCache<ReferrerLeaderboard>>
    • initializeCyclesCaches() creates independent SWRCache for each configured cycle
    • Followed previous cache implementation pattern
    • Independent failure handling (SWR behavior preserved per cycle)
  2. V1 Middleware (apps/ensapi/src/middleware/referrer-leaderboard.middleware-v1.ts):

    • Provides referralLeaderboardCyclesCaches to downstream handlers
    • Followed previous middleware implementation pattern

4. V1 API Routes (apps/ensapi/src/handlers/ensanalytics-api-v1.ts)

  1. GET /v1/ensanalytics/referral-leaderboard (renamed from /referrers):

    • New required cycle query parameter
    • Returns 404 if cycle not found (with list of valid cycles)
    • Returns 500 if specific cycle's cache failed
    • Returns paginated leaderboard page for requested cycle
  2. GET /v1/ensanalytics/referral-leaderboard/:referrer (renamed from /referrers/:referrer):

    • Returns ReferrerDetailAllCyclesData instead of single ReferrerDetail
    • Aggregates data across ALL configured cycles
    • Returns 500 if any cycle's cache failed
    • Supports custom cycles loaded from JSON

Design & Planning

How this approach was chosen
  • Each cycle needs its own independent leaderboard (different time ranges, rules, award pools)
  • Per-cycle caching enables independent failure semantics (cycle 1 failing doesn't break cycle 2)

Self-Review

What you caught yourself
  • Some things noticed by CodeRabbit and others, like not using proper types in documentation examples (e.g. wrong format of PriceUsdc in custom cycles json)
  • Bugs caught: None
  • Logic simplified: None
  • Naming / terminology improved: None
  • Dead or unnecessary code removed: ENS_HOLIDAY_AWARDS_START/END env vars from ensapi config, now depends on the referral program cycle custom json

Cross-Codebase Alignment

Related code you checked
  • v0 unchanged, except the env variables (replaced by importing constants)
  • Search terms used: ReferralProgram, ENS_HOLIDAY_AWARDS, referrerLeaderboard, SWRCache, cycle, v1, v0
  • Reviewed but unchanged: v0 API implementation
  • Deferred alignment: None

Downstream & Consumer Impact

Who this affects and how

v0 unaffected, v1 is allowed to work with multiple cycles

  • Public APIs affected: v1 routes only (v0 unchanged)
  • Docs updated: Added comprehensive .env.local.example documentation, same with the client

Testing Evidence

How this was validated
  • Normal CI testing
  • Manually tested with running ENSApi instance
  • Tested custom JSON reading with the local server and multiple types of JSONs (duplicate ids, not a json, wrong format, correct format: removing, overriding, adding)
  • Testing performed: Automated CI + comprehensive manual validation
  • Known gaps: None
  • What reviewers have to reason about manually: Handling of custom cycle ids in terms of types

Scope Reductions

What you intentionally didn't do

Did not do partial success response for referrer details (all cycles) - if one cycle fails, return 500.

  • Follow-ups: Since all cycles essentially depend on the same data from the database, we could try to cache it as well, so we wouldn't repeat the same database queries for each cycle.
  • Why they were deferred: This is not a priority for now, and it is not clearly defined yet if the query will remain the same for the next cycles.

Risk Analysis

How this could go wrong

Low risk - v0 affected very little (only env vars, which were not used in production either way and defaulted to constants, which are now the only source of truth).

V1 is not used by any downstream consumer yet.

  • Risk areas:
    • Configuration loading (easy to crash for custom JSON if request fails, but this should be only limited to us testing the ENSApi)
  • Mitigations or rollback options: Revertible through previous ENSApi version
  • Named owner if this causes problems: @Goader

Pre-Review Checklist (Blocking)

  • I reviewed every line of this diff and understand it end-to-end
  • I'm prepared to defend this PR line-by-line in review
  • I'm comfortable being the on-call owner for this change
  • Relevant changesets are included (or explicitly not required)

@Goader Goader self-assigned this Feb 3, 2026
Copilot AI review requested due to automatic review settings February 3, 2026 14:55
@changeset-bot
Copy link

changeset-bot bot commented Feb 3, 2026

🦋 Changeset detected

Latest commit: e6dddc7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
Name Type
@namehash/ens-referrals Major
ensapi Major
@ensnode/ensnode-sdk Major
ensadmin Major
ensindexer Major
ensrainbow Major
fallback-ensapi Major
@ensnode/ensnode-react Major
@ensnode/ensrainbow-sdk Major
@namehash/namehash-ui Major
@ensnode/datasources Major
@ensnode/ponder-metadata Major
@ensnode/ensnode-schema Major
@ensnode/ponder-sdk Major
@ensnode/ponder-subgraph Major
@ensnode/shared-configs Major
@docs/ensnode Major
@docs/ensrainbow Major
@docs/mintlify Major

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Contributor

vercel bot commented Feb 3, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment Feb 7, 2026 8:27pm
ensnode.io Ready Ready Preview, Comment Feb 7, 2026 8:27pm
ensrainbow.io Ready Ready Preview, Comment Feb 7, 2026 8:27pm

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

This PR implements referral program cycles support, replacing hardcoded ENS Holiday Awards configuration with a flexible, multi-cycle architecture. It introduces cycle definitions, per-cycle leaderboard caching, URL-based cycle configuration loading, and updated API endpoints to support querying data by cycle slug.

Changes

Cohort / File(s) Summary
Cycle Data Models
packages/ens-referrals/src/v1/cycle.ts, packages/ens-referrals/src/v1/cycle-defaults.ts
New types for cycle slugs, cycle config, and cycle config sets; default cycle factory generating pre-configured 2025-12 and 2026-03 cycles.
Configuration & Environment
packages/ensnode-sdk/src/shared/config/environments.ts, apps/ensapi/src/config/config.schema.ts, apps/ensapi/src/config/config.schema.test.ts, apps/ensapi/src/config/environment.ts, apps/ensapi/src/config/validations.ts, apps/ensapi/.env.local.example
Replaced ENS_HOLIDAY_AWARDS_START/END with CUSTOM_REFERRAL_PROGRAM_CYCLES URL-based configuration; removed holiday date validation; updated config schema to parse custom cycle config URL.
Cycle Caching & Loading
apps/ensapi/src/cache/referral-program-cycle-set.cache.ts, apps/ensapi/src/cache/referral-leaderboard-cycles.cache.ts
New cache for loading cycle configs from URL or default; new per-cycle leaderboard cache builder supporting independent caches for each cycle with validation and error handling.
Middleware
apps/ensapi/src/middleware/referral-program-cycle-set.middleware.ts, apps/ensapi/src/middleware/referral-leaderboard-cycles-caches.middleware.ts, apps/ensapi/src/lib/hono-factory.ts
New middleware for loading and exposing cycle configs and per-cycle leaderboard caches in request context.
API Routes & Handlers
apps/ensapi/src/handlers/ensanalytics-api-v1.ts, apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts
Renamed /referrers to /referral-leaderboard with mandatory cycle query param; updated /referrer/:referrer to return per-cycle data; added /cycles endpoint; added 404 for unknown cycles and 503 for unavailable cache data.
Serialization & Deserialization
packages/ens-referrals/src/v1/api/serialize.ts, packages/ens-referrals/src/v1/api/deserialize.ts, packages/ens-referrals/src/v1/api/serialized-types.ts, packages/ens-referrals/src/v1/api/types.ts, packages/ens-referrals/src/v1/api/zod-schemas.ts
Added serializers/deserializers for cycle configs and per-cycle responses; new request/response types for cycle-based queries; updated schemas to validate cycle slugs and cycle arrays with deduplication.
Client & Public API
packages/ens-referrals/src/v1/client.ts, packages/ens-referrals/src/v1/index.ts
Added static method to load cycle config sets from URL; new method to fetch cycle configs; updated leaderboard and detail methods to support cycle parameters with per-cycle response handling.
Rules & Constants
packages/ens-referrals/src/v1/rules.ts, packages/ens-referrals/src/v1/leaderboard-page.test.ts, apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/get-referrer-leaderboard-v1.test.ts, apps/ensapi/src/lib/ensanalytics/referrer-leaderboard/mocks-v1.ts
Removed ENS Holiday Awards constants; added rulesUrl to ReferralProgramRules; updated buildReferralProgramRules to accept rulesUrl parameter; updated mocks and tests with new field.
Cache Infrastructure
packages/ensnode-sdk/src/shared/cache/swr-cache.ts, packages/ensnode-sdk/src/shared/cache/swr-cache.test.ts
Added optional errorTtl property to SWRCacheOptions for separate TTL on cached errors; updated error handling and TTL computation to support error-specific caching.
Utilities
packages/ensnode-sdk/src/shared/datetime.ts, packages/ensnode-sdk/src/shared/datetime.test.ts
Added parseTimestamp() function to convert ISO 8601 date strings to Unix timestamps.
Cleanup
apps/ensapi/src/cache/referrer-leaderboard.cache-v1.ts, apps/ensapi/src/index.ts, apps/ensapi/src/middleware/referrer-leaderboard.middleware-v1.ts
Deleted single-cycle leaderboard cache module; updated graceful shutdown to destroy per-cycle caches instead; updated middleware to expose per-cycle cache map.
Changesets
.changeset/clever-laws-count.md
Changeset documenting minor version bumps for referrals package and ensapi, patch for ensnode-sdk.

Sequence Diagram

sequenceDiagram
    participant Client
    participant ENSApi
    participant CycleSetCache
    participant CyclesMiddleware
    participant PerCycleCaches
    participant Leaderboard DB

    Client->>ENSApi: GET /v1/ensanalytics/referral-leaderboard?cycle=2025-12
    ENSApi->>CycleSetCache: read() cycle configs
    CycleSetCache->>ENSApi: ReferralProgramCycleConfigSet
    ENSApi->>CyclesMiddleware: validate cycle exists
    alt Cycle not found
        ENSApi->>Client: 404 Unknown cycle
    else Cycle found
        ENSApi->>PerCycleCaches: getReferralLeaderboardCyclesCaches()
        PerCycleCaches->>Leaderboard DB: fetch 2025-12 leaderboard
        Leaderboard DB->>PerCycleCaches: ReferrerLeaderboard
        PerCycleCaches->>ENSApi: cached leaderboard
        ENSApi->>Client: 200 ReferrerLeaderboardPageResponse
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • Introduce Referral Program Cycles #1522: Introduces Referral Program Cycles — This PR directly addresses all the goals and action items from this issue, implementing multi-cycle architecture, cycle definitions, configuration loading, and API updates for per-cycle leaderboard and detail queries.

Possibly related PRs

Suggested labels

ensanalytics, ensnode-sdk


🐰 Cycles spin 'round, each with its own dance,
From December's gleam to March's next chance,
Per-cycle caches hum their SWR song,
One config flows where many belong,
The leaderboard blooms in cycles of grace!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Referral Program Cycles' clearly summarizes the main change in the PR, which is adding multi-cycle support to the ENS Referrals API v1.
Description check ✅ Passed The PR description extensively covers the changes with detailed sections on new data models, configuration system, caching architecture, and API routes, far exceeding template expectations.
Linked Issues check ✅ Passed All major requirements from issue #1522 are implemented: cycle types, default cycles, Zod validation, config loading with custom URL support, multi-cycle middleware, and updated v1 routes with cycle parameters.
Out of Scope Changes check ✅ Passed All changes directly support multi-cycle support in v1 API. V0 remains unchanged except for replacing env vars with constants. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/referral-program-cycles

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

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 pull request refactors the ENS referral program system from supporting a single program period to supporting multiple concurrent and sequential "cycles". Each cycle represents a distinct referral program period with its own rules, leaderboard, and award distribution.

Changes:

  • Introduces a cycle-based architecture with ReferralProgramCycle type and ReferralProgramCycleSet map structure
  • Updates API endpoints to require cycle ID parameter: /v1/ensanalytics/referral-leaderboard?cycle=<id> and /v1/ensanalytics/referral-leaderboard/<address> (now returns data for all cycles)
  • Replaces single cache with per-cycle independent caches to allow individual cycle failures without affecting others
  • Removes hardcoded ENS Holiday Awards configuration from environment variables, replacing with optional CUSTOM_REFERRAL_PROGRAM_CYCLES URL
  • Adds Cycle 2 (March 2026) as a second default cycle alongside Cycle 1 (ENS Holiday Awards December 2025)

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/ensnode-sdk/src/shared/config/environments.ts Replaces EnsHolidayAwardsEnvironment with ReferralProgramCyclesEnvironment
packages/ens-referrals/src/v1/rules.ts Removes hardcoded ENS Holiday Awards constants
packages/ens-referrals/src/v1/cycle.ts Adds new cycle type definitions and type guards
packages/ens-referrals/src/v1/cycle-defaults.ts Defines default cycles (Cycle 1 and Cycle 2) with configuration
packages/ens-referrals/src/v1/index.ts Exports new cycle types
packages/ens-referrals/src/v1/client.ts Updates SDK client to require cycle parameter for leaderboard queries
packages/ens-referrals/src/v1/api/*.ts Updates API types to support all-cycles responses and cycle-specific requests
apps/ensapi/src/handlers/ensanalytics-api-v1.ts Updates API handlers to support cycle parameter and multi-cycle responses
apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts Updates tests to work with new cycle-based cache structure
apps/ensapi/src/cache/referral-leaderboard-cycles.cache.ts New cache implementation with per-cycle independent caches
apps/ensapi/src/cache/referrer-leaderboard.cache-v1.ts Deleted - replaced by cycle-based cache
apps/ensapi/src/config/*.ts Updates configuration loading to support custom cycle URLs
apps/ensapi/.env.local.example Updates environment variable documentation
.changeset/clever-laws-count.md Empty changeset file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ens-referrals/src/v1/api/types.ts (1)

74-124: ⚠️ Potential issue | 🟠 Major

Breaking public type renames—add compatibility aliases or ensure a major version bump.
Downstream TypeScript consumers using the old ReferrerDetailResponse* exports will break. Consider re‑exporting deprecated aliases or explicitly bumping the package major version and documenting the migration.

🧩 Optional compatibility aliases
+/** `@deprecated` Use ReferrerDetailAllCyclesResponseCodes */
+export const ReferrerDetailResponseCodes = ReferrerDetailAllCyclesResponseCodes;
+/** `@deprecated` Use ReferrerDetailAllCyclesResponseCode */
+export type ReferrerDetailResponseCode = ReferrerDetailAllCyclesResponseCode;
+/** `@deprecated` Use ReferrerDetailAllCyclesResponseOk */
+export type ReferrerDetailResponseOk = ReferrerDetailAllCyclesResponseOk;
+/** `@deprecated` Use ReferrerDetailAllCyclesResponseError */
+export type ReferrerDetailResponseError = ReferrerDetailAllCyclesResponseError;
+/** `@deprecated` Use ReferrerDetailAllCyclesResponse */
+export type ReferrerDetailResponse = ReferrerDetailAllCyclesResponse;
🤖 Fix all issues with AI agents
In `@apps/ensapi/.env.local.example`:
- Around line 115-138: Update the env example comment for
CUSTOM_REFERRAL_PROGRAM_CYCLES to state that supplying a URL will completely
replace the default referral cycle set (i.e., it overrides defaults), and
instruct operators to include every cycle they want active in their hosted JSON;
reference the env variable name CUSTOM_REFERRAL_PROGRAM_CYCLES and the example
JSON structure in the comment so readers know to add any default cycles they
still want when providing a custom file.

In `@apps/ensapi/src/handlers/ensanalytics-api-v1.ts`:
- Around line 185-198: The handler currently fail-fast when any cycle
cache.read() returns an Error; to support partial-success change the response to
allow per-cycle results and errors by updating the
ReferrerDetailAllCyclesResponse/ReferrerDetailAllCyclesData types to map cycleId
-> { data?: ReferrerDetail; error?: string }, then modify the loop over
referralLeaderboardCyclesCaches to collect each cycle's result into
allCyclesData[cycleId] using getReferrerDetail(referrer, leaderboard) on success
or an error string on failure (using cycleCache.read()), and finally return a
200 with the aggregated per-cycle data (and optionally an overall status field)
instead of returning 500 on the first failure so clients can consume available
cycles while seeing which cycles failed.

In `@packages/ens-referrals/src/v1/api/deserialize.ts`:
- Around line 235-244: The type error comes from building `data` via
`Object.entries(maybeResponse.data)` which TypeScript widens to `Record<string,
...>`; fix by ensuring the final object is explicitly typed as
`ReferrerDetailAllCyclesResponse` — replace the assignment to `deserialized`
with an explicit cast: set `deserialized = { responseCode: "ok", data } as
ReferrerDetailAllCyclesResponse`; alternatively, build `data` with a typed
reducer (e.g., reduce to `Record<ReferralProgramCycleId, ReferrerDetail>` or
`ReferrerDetailAllCyclesData`) so the resulting `data` has the correct key type
before assigning to `deserialized` (use `deserializeReferrerDetail` inside the
reducer).

In `@packages/ens-referrals/src/v1/api/serialize.ts`:
- Around line 154-160: The switch in serializeReferrerDetail over ReferrerDetail
isn't exhaustive and can return undefined if a new detail.type is added; update
serializeReferrerDetail to include a default branch that asserts exhaustiveness
(e.g., call an assertNever/assertUnreachable helper or assign detail to a
never-typed variable and throw) so the compiler errors on new union members and
at runtime you throw a clear error; keep existing branches that call
serializeReferrerDetailRanked and serializeReferrerDetailUnranked and reference
ReferrerDetail/SerializedReferrerDetail in the assertion.
- Around line 201-218: The switch over response.responseCode
(ReferrerDetailAllCyclesResponseCodes) is not exhaustive and the serializedData
is unsafely asserted with {} as SerializedReferrerDetailAllCyclesData; update
the case for Ok to build the record in a type-safe way (e.g., const
serializedData: Partial<SerializedReferrerDetailAllCyclesData> = {}; for (const
[cycleId, detail] of Object.entries(response.data)) { serializedData[cycleId as
ReferralProgramCycleId] = serializeReferrerDetail(detail); } and return data:
serializedData as SerializedReferrerDetailAllCyclesData), and add a default (or
never) branch after the known cases that throws an error like throw new
Error(`Unhandled ReferrerDetailAllCyclesResponseCodes:
${response.responseCode}`) to enforce exhaustiveness for
ReferrerDetailAllCyclesResponseCodes.

In `@packages/ens-referrals/src/v1/api/zod-schemas.ts`:
- Around line 281-306: In makeReferralProgramCycleSetSchema, the refine
currently only checks that each value parses as a ReferralProgramCycle but
doesn't ensure the Map key equals the cycle's id; update the validate loop in
makeReferralProgramCycleSetSchema to parse each value with
makeReferralProgramCycleSchema(`${valueLabel}[${key}]`), extract the parsed
cycle.id, and return false if typeof key !== "string" or parsedCycle.id !== key
(so keys must match the cycle.id). Also update the refine error message to
indicate keys must equal the nested cycle.id for integrity.

In `@packages/ens-referrals/src/v1/client.ts`:
- Around line 220-229: The example accesses response.data["cycle-1"] without
guarding for undefined which can cause unsafe access; update the example around
the getReferrerDetail response handling (check response.responseCode ===
ReferrerDetailAllCyclesResponseCodes.Ok) to first confirm the key exists (e.g.,
verify response.data["cycle-1"] is truthy) before reading its .type and
.referrer, and adjust the branching that inspects ReferrerDetailTypeIds.Ranked
to handle the case where cycle1Detail is undefined so callers see the safer
pattern.
- Around line 99-104: The JSDoc example in client.ts references a non-existent
rules.cycleId; update the example to use an existing serialized field from
serializeReferralProgramRules (e.g., rules.subregistryId or
rules.startTime/endTime/totalAwardPoolValue) or, if you intended to show the
requested cycle id, reference the request parameter (e.g., the cycleId variable
used to call the client) instead; modify the example console.log lines in the
example block near the response handling to use one of those valid identifiers
so the docs match serializeReferralProgramRules and the request parameters.

In `@packages/ens-referrals/src/v1/cycle-defaults.ts`:
- Around line 45-55: The START_DATE and END_DATE constants in cycle-defaults.ts
are using incorrect Unix timestamps for the documented UTC dates; update
START_DATE (currently 1772524800) to 1772323200 and END_DATE (currently
1775116799) to 1775001599 so the cycle properly covers 2026-03-01T00:00:00Z
through 2026-03-31T23:59:59Z; edit the START_DATE and END_DATE values (the
symbols START_DATE and END_DATE) to the corrected integers and keep the
UnixTimestamp type assertions.

In `@packages/ens-referrals/src/v1/cycle.ts`:
- Around line 42-43: The type guard isPredefinedCycleId currently narrows to
ReferralProgramCycleId which is effectively any string; change its return type
to the concrete union of predefined IDs (e.g. value is typeof
ALL_REFERRAL_PROGRAM_CYCLE_IDS[number] or a named
PredefinedReferralProgramCycleId union) and keep the runtime check using
ALL_REFERRAL_PROGRAM_CYCLE_IDS.includes(value as any) so callers get stronger
compile-time narrowing while preserving the existing runtime behavior; update
the function signature only (isPredefinedCycleId) to reference the more precise
union type (or create a named union) and leave ALL_REFERRAL_PROGRAM_CYCLE_IDS
usage unchanged.

Comment on lines 42 to 43
export const isPredefinedCycleId = (value: string): value is ReferralProgramCycleId =>
ALL_REFERRAL_PROGRAM_CYCLE_IDS.includes(value as ReferralProgramCycleId);
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider a more precise return type for the type guard.

The type guard narrows to ReferralProgramCycleId, but since that type includes string & {}, any string technically satisfies it. For better type safety, consider narrowing to just the predefined union:

♻️ Suggested refinement
-export const isPredefinedCycleId = (value: string): value is ReferralProgramCycleId =>
+export const isPredefinedCycleId = (value: string): value is (typeof ReferralProgramCycleIds)[keyof typeof ReferralProgramCycleIds] =>
   ALL_REFERRAL_PROGRAM_CYCLE_IDS.includes(value as ReferralProgramCycleId);

The current implementation works correctly at runtime, and the JSDoc clearly documents the behavior, so this is optional.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const isPredefinedCycleId = (value: string): value is ReferralProgramCycleId =>
ALL_REFERRAL_PROGRAM_CYCLE_IDS.includes(value as ReferralProgramCycleId);
export const isPredefinedCycleId = (value: string): value is (typeof ReferralProgramCycleIds)[keyof typeof ReferralProgramCycleIds] =>
ALL_REFERRAL_PROGRAM_CYCLE_IDS.includes(value as ReferralProgramCycleId);
🤖 Prompt for AI Agents
In `@packages/ens-referrals/src/v1/cycle.ts` around lines 42 - 43, The type guard
isPredefinedCycleId currently narrows to ReferralProgramCycleId which is
effectively any string; change its return type to the concrete union of
predefined IDs (e.g. value is typeof ALL_REFERRAL_PROGRAM_CYCLE_IDS[number] or a
named PredefinedReferralProgramCycleId union) and keep the runtime check using
ALL_REFERRAL_PROGRAM_CYCLE_IDS.includes(value as any) so callers get stronger
compile-time narrowing while preserving the existing runtime behavior; update
the function signature only (isPredefinedCycleId) to reference the more precise
union type (or create a named union) and leave ALL_REFERRAL_PROGRAM_CYCLE_IDS
usage unchanged.

@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 3, 2026 16:00 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 3, 2026 16:00 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 3, 2026 16:00 Inactive
@vercel
Copy link
Contributor

vercel bot commented Feb 3, 2026

Deployment failed with the following error:

Failed to create deployment for team_CWAzsA7nfejZhqJ4NfJ8B5ew in project prj_nKcHTO12hq9kcgascQMq4xokRhwp: FetchError: request to https://76.76.21.112/v13/now/deployments?ownerId=team_CWAzsA7nfejZhqJ4NfJ8B5ew&projectId=prj_nKcHTO12hq9kcgascQMq4xokRhwp&skipAutoDetectionConfirmation=1&teamId=team_CWAzsA7nfejZhqJ4NfJ8B5ew&traceCarrier=%7B%22ot-baggage-webhookAt%22%3A%221770132262052%22%2C%22ot-baggage-senderUsername%22%3A%22gh.Goader%22%2C%22x-datadog-trace-id%22%3A%228401127708701102998%22%2C%22x-datadog-parent-id%22%3A%222369066029094997530%22%2C%22x-datadog-sampling-priority%22%3A%222%22%2C%22x-datadog-tags%22%3A%22_dd.p.tid%3D6982132600000000%2C_dd.p.dm%3D-3%22%2C%22traceparent%22%3A%2200-69821326000000007496cd23d5d21396-20e09d0069a70e1a-01%22%2C%22tracestate%22%3A%22dd%3Dt.tid%3A6982132600000000%3Bt.dm%3A-3%3Bs%3A2%3Bp%3A20e09d0069a70e1a%22%7D failed, reason: socket hang up

@vercel
Copy link
Contributor

vercel bot commented Feb 3, 2026

Deployment failed with the following error:

Failed to create deployment for team_CWAzsA7nfejZhqJ4NfJ8B5ew in project prj_fqPGNyBo1bNMz2uZKsCVNdKsoOJF: FetchError: request to https://76.76.21.112/v13/now/deployments?ownerId=team_CWAzsA7nfejZhqJ4NfJ8B5ew&projectId=prj_fqPGNyBo1bNMz2uZKsCVNdKsoOJF&skipAutoDetectionConfirmation=1&teamId=team_CWAzsA7nfejZhqJ4NfJ8B5ew&traceCarrier=%7B%22ot-baggage-webhookAt%22%3A%221770132262052%22%2C%22ot-baggage-senderUsername%22%3A%22gh.Goader%22%2C%22x-datadog-trace-id%22%3A%228401127708701102998%22%2C%22x-datadog-parent-id%22%3A%22579946873202667545%22%2C%22x-datadog-sampling-priority%22%3A%222%22%2C%22x-datadog-tags%22%3A%22_dd.p.tid%3D6982132600000000%2C_dd.p.dm%3D-3%22%2C%22traceparent%22%3A%2200-69821326000000007496cd23d5d21396-080c629bc5d7f019-01%22%2C%22tracestate%22%3A%22dd%3Dt.tid%3A6982132600000000%3Bt.dm%3A-3%3Bs%3A2%3Bp%3A080c629bc5d7f019%22%7D failed, reason: socket hang up

@vercel
Copy link
Contributor

vercel bot commented Feb 3, 2026

Deployment failed with the following error:

Failed to create deployment for team_CWAzsA7nfejZhqJ4NfJ8B5ew in project prj_IdDwohGX1owW84Tj9u3nFc6vDekK: FetchError: request to https://76.76.21.112/v13/now/deployments?ownerId=team_CWAzsA7nfejZhqJ4NfJ8B5ew&projectId=prj_IdDwohGX1owW84Tj9u3nFc6vDekK&skipAutoDetectionConfirmation=1&teamId=team_CWAzsA7nfejZhqJ4NfJ8B5ew&traceCarrier=%7B%22ot-baggage-webhookAt%22%3A%221770132262052%22%2C%22ot-baggage-senderUsername%22%3A%22gh.Goader%22%2C%22x-datadog-trace-id%22%3A%228401127708701102998%22%2C%22x-datadog-parent-id%22%3A%222369066029094997530%22%2C%22x-datadog-sampling-priority%22%3A%222%22%2C%22x-datadog-tags%22%3A%22_dd.p.tid%3D6982132600000000%2C_dd.p.dm%3D-3%22%2C%22traceparent%22%3A%2200-69821326000000007496cd23d5d21396-20e09d0069a70e1a-01%22%2C%22tracestate%22%3A%22dd%3Dt.tid%3A6982132600000000%3Bt.dm%3A-3%3Bs%3A2%3Bp%3A20e09d0069a70e1a%22%7D failed, reason: socket hang up

Copilot AI review requested due to automatic review settings February 3, 2026 16:20
Copy link
Contributor

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

Copilot reviewed 25 out of 25 changed files in this pull request and generated 10 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 83 to 127
async function loadReferralProgramCycleSet(
customCyclesUrl: string | undefined,
namespace: ENSNamespaceId,
): Promise<ReferralProgramCycleSet> {
const subregistryId = getEthnamesSubregistryId(namespace);

if (!customCyclesUrl) {
logger.info("Using default referral program cycle set");
return getReferralProgramCycleSet(subregistryId.address);
}

// Validate URL format
try {
new URL(customCyclesUrl);
} catch {
throw new Error(`CUSTOM_REFERRAL_PROGRAM_CYCLES is not a valid URL: ${customCyclesUrl}`);
}

// Fetch and validate
logger.info(`Fetching custom referral program cycles from: ${customCyclesUrl}`);
const response = await fetch(customCyclesUrl);
if (!response.ok) {
throw new Error(
`Failed to fetch custom referral program cycles from ${customCyclesUrl}: ${response.status} ${response.statusText}`,
);
}

const json = await response.json();
const schema = makeCustomReferralProgramCyclesSchema("CUSTOM_REFERRAL_PROGRAM_CYCLES");
const validated = schema.parse(json);

// Convert array to Map, check for duplicates
const cycleSet: ReferralProgramCycleSet = new Map();
for (const cycleObj of validated) {
const cycle = cycleObj as ReferralProgramCycle;
const cycleId = cycle.id;
if (cycleSet.has(cycleId)) {
throw new Error(`Duplicate cycle ID in CUSTOM_REFERRAL_PROGRAM_CYCLES: ${cycle.id}`);
}
cycleSet.set(cycleId, cycle);
}

logger.info(`Loaded ${cycleSet.size} custom referral program cycles`);
return cycleSet;
}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The loadReferralProgramCycleSet function performs a network request during application startup (via buildConfigFromEnvironment). If the custom cycles URL is unreachable, slow to respond, or returns invalid data, this will block application startup.

Consider adding:

  1. A timeout for the fetch operation to prevent hanging
  2. Retry logic with exponential backoff for transient network failures
  3. Better error context in the exception messages to help with debugging
  4. Optional: A fallback mechanism to use default cycles if custom cycles fail to load (with appropriate logging)

This would improve resilience during deployment and prevent startup failures due to temporary network issues.

Copilot uses AI. Check for mistakes.
Comment on lines 184 to 198
// Check all caches and fail immediately if any cache failed
for (const [cycleId, cycleCache] of c.var.referralLeaderboardCyclesCaches) {
const leaderboard = await cycleCache.read();
if (leaderboard instanceof Error) {
return c.json(
serializeReferrerDetailAllCyclesResponse({
responseCode: ReferrerDetailAllCyclesResponseCodes.Error,
error: "Internal Server Error",
errorMessage: `Referrer leaderboard data for cycle ${cycleId} has not been successfully cached yet.`,
} satisfies ReferrerDetailAllCyclesResponse),
500,
);
}
allCyclesData[cycleId] = getReferrerDetail(referrer, leaderboard);
}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The fail-fast approach means that if any single cycle fails to load, the entire request returns an error. This could be problematic from a user experience perspective - if cycle-1 is working but cycle-2 fails, users cannot access cycle-1 data either.

Consider implementing a more resilient approach where:

  1. Successfully loaded cycles are returned in the response
  2. Failed cycles are indicated with an error status or omitted with a warning
  3. The response includes information about which cycles loaded successfully vs. which failed

This would provide better availability and user experience, especially during partial outages or when one cycle has data issues.

Copilot uses AI. Check for mistakes.
Comment on lines 185 to 198
for (const [cycleId, cycleCache] of c.var.referralLeaderboardCyclesCaches) {
const leaderboard = await cycleCache.read();
if (leaderboard instanceof Error) {
return c.json(
serializeReferrerDetailAllCyclesResponse({
responseCode: ReferrerDetailAllCyclesResponseCodes.Error,
error: "Internal Server Error",
errorMessage: `Referrer leaderboard data for cycle ${cycleId} has not been successfully cached yet.`,
} satisfies ReferrerDetailAllCyclesResponse),
500,
);
}
allCyclesData[cycleId] = getReferrerDetail(referrer, leaderboard);
}
Copy link

Copilot AI Feb 3, 2026

Choose a reason for hiding this comment

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

The code iterates over c.var.referralLeaderboardCyclesCaches using a for-of loop with .entries(). JavaScript Maps preserve insertion order, but the iteration order matters here because the first cycle that fails will determine the error response.

If the goal is to fail fast on any cycle failure, consider:

  1. Documenting the specific iteration order behavior (e.g., "cycles are checked in the order they were configured")
  2. Or explicitly sorting the cycles to ensure a predictable error reporting order
  3. Or collecting all failures and reporting them together

The current implementation may result in non-deterministic error reporting if the Map insertion order isn't guaranteed by the configuration loading code.

Copilot uses AI. Check for mistakes.
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 4, 2026 02:23 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 4, 2026 02:23 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 4, 2026 02:23 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 4, 2026 02:26 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 4, 2026 02:26 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 4, 2026 02:26 Inactive
@Goader Goader marked this pull request as ready for review February 4, 2026 02:27
@Goader Goader requested a review from a team as a code owner February 4, 2026 02:27
Copy link
Contributor

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

Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

/**
* URL to the rules for this cycle.
*/
RULES_URL: "https://ensawards.org/ens-holiday-awards-rules",
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The RULES_URL for Cycle 2 (March 2026) points to "https://ensawards.org/ens-holiday-awards-rules", which appears to be specific to Cycle 1 (ENS Holiday Awards). Consider whether Cycle 2 should have its own dedicated rules URL, such as "https://ensawards.org/march-2026-rules" or a more generic URL.

Suggested change
RULES_URL: "https://ensawards.org/ens-holiday-awards-rules",
RULES_URL: "https://ensawards.org/rules",

Copilot uses AI. Check for mistakes.
Comment on lines 83 to 152
async function loadReferralProgramCycleSet(
customCyclesUrl: string | undefined,
namespace: ENSNamespaceId,
): Promise<ReferralProgramCycleSet> {
const subregistryId = getEthnamesSubregistryId(namespace);

if (!customCyclesUrl) {
logger.info("Using default referral program cycle set");
return getReferralProgramCycleSet(subregistryId);
}

// Validate URL format
try {
new URL(customCyclesUrl);
} catch {
throw new Error(`CUSTOM_REFERRAL_PROGRAM_CYCLES is not a valid URL: ${customCyclesUrl}`);
}

// Fetch and validate
logger.info(`Fetching custom referral program cycles from: ${customCyclesUrl}`);

let response: Response;
try {
response = await fetch(customCyclesUrl);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(
`Failed to fetch custom referral program cycles from ${customCyclesUrl}: ${errorMessage}. ` +
`Please verify the URL is accessible and the server is running.`,
);
}

if (!response.ok) {
throw new Error(
`Failed to fetch custom referral program cycles from ${customCyclesUrl}: ${response.status} ${response.statusText}`,
);
}

let json: unknown;
try {
json = await response.json();
} catch (_error) {
throw new Error(
`Failed to parse JSON from ${customCyclesUrl}: The response is not valid JSON. ` +
`Please verify the file contains valid JSON.`,
);
}

const schema = makeCustomReferralProgramCyclesSchema("CUSTOM_REFERRAL_PROGRAM_CYCLES");
const result = schema.safeParse(json);

if (result.error) {
throw new Error(
`Failed to validate custom referral program cycles from ${customCyclesUrl}:\n${prettifyError(result.error)}\n` +
`Please verify the JSON structure matches the expected schema.`,
);
}

const validated = result.data;

const cycleSet: ReferralProgramCycleSet = new Map();
for (const cycleObj of validated) {
const cycle = cycleObj as ReferralProgramCycle;
const cycleId = cycle.id;
cycleSet.set(cycleId, cycle);
}

logger.info(`Loaded ${cycleSet.size} custom referral program cycles`);
return cycleSet;
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The loadReferralProgramCycleSet function, which handles loading custom cycles from a URL, lacks test coverage. This is a complex async function with multiple error paths (invalid URL, fetch failure, JSON parse failure, validation failure). Consider adding unit tests to ensure all error cases are properly handled and provide helpful error messages.

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/ensapi/src/handlers/ensanalytics-api-v1.ts (1)

1-222: ⚠️ Potential issue | 🟡 Minor

Formatter check failed for this file.

Please run the repo formatter and re-commit to satisfy the CI formatting step.

apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts (1)

1-536: ⚠️ Potential issue | 🟡 Minor

Formatter check failed for this file.

Please run the repo formatter and re-commit to satisfy the CI formatting step.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 4, 2026

Greptile Overview

Greptile Summary

This PR successfully implements multi-cycle support for the ENS Referrals API v1, allowing the system to manage multiple referral program periods with independent leaderboards, rules, and caching.

Key Changes

  • Type System: introduced extensible ReferralProgramCycleId type using string & {} pattern, providing autocomplete for known cycles (cycle-1, cycle-2) while accepting custom cycle IDs
  • Configuration: added CUSTOM_REFERRAL_PROGRAM_CYCLES environment variable to load cycle definitions from remote JSON, with comprehensive validation and error handling
  • Cache Architecture: replaced single v1 cache with per-cycle independent SWRCache instances in ReferralLeaderboardCyclesCacheMap, ensuring isolated failure handling
  • API Routes:
    • /referral-leaderboard now requires cycle query parameter, returns 404 for unknown cycles
    • /referral-leaderboard/:referrer returns aggregated data across all cycles, fails fast if any cycle cache is unavailable
  • Backward Compatibility: v0 API unchanged except for importing constants from @namehash/ens-referrals instead of environment variables

Strengths

  • Comprehensive test coverage including edge cases (404 handling, cache failures, empty leaderboards)
  • Proper Zod validation with duplicate ID detection
  • Clear error messages with actionable information
  • Independent cache failure semantics prevent cascading failures
  • Well-documented configuration with examples in .env.local.example

Observations

  • The fail-fast approach for /referral-leaderboard/:referrer means if any single cycle cache fails, the entire endpoint returns 500 even if other cycles are healthy (acknowledged in PR description as intentional scope reduction)
  • Configuration loading crashes the server on invalid custom JSON (appropriate for startup-time validation)
  • The string & {} TypeScript pattern for extensible types is clever but may be unfamiliar to some developers

Confidence Score: 4.5/5

  • safe to merge with minimal risk - v0 unaffected, v1 not yet in production, comprehensive tests cover edge cases
  • well-architected multi-cycle implementation with proper isolation, validation, and testing; minor deduction for fail-fast behavior on referrer detail endpoint which could be partial success instead
  • No files require special attention - implementation is solid across all changed files

Important Files Changed

Filename Overview
packages/ens-referrals/src/v1/cycle.ts introduces core type definitions for referral program cycles with extensible ID typing using string & {} pattern
packages/ens-referrals/src/v1/api/zod-schemas.ts adds comprehensive validation schemas for cycles, with proper duplicate ID detection and Map structure validation
apps/ensapi/src/config/config.schema.ts implements async configuration loading with URL fetching, JSON validation, and comprehensive error handling for custom cycles
apps/ensapi/src/cache/referral-leaderboard-cycles.cache.ts creates independent SWRCache instances per cycle for isolated failure handling
apps/ensapi/src/handlers/ensanalytics-api-v1.ts adds cycle parameter to leaderboard endpoint and returns aggregated data across all cycles for referrer detail endpoint
apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts comprehensive test coverage for multi-cycle scenarios including 404 handling and cache failure edge cases

Sequence Diagram

sequenceDiagram
    participant Client as API Client
    participant API as ENSApi v1
    participant Config as Config Loader
    participant Cache as Cycle Caches
    participant DB as Database
    
    Note over Config: Startup: Load Configuration
    Config->>Config: loadReferralProgramCycleSet()
    alt Custom cycles URL provided
        Config->>Config: Fetch JSON from URL
        Config->>Config: Validate with Zod schema
        Config->>Config: Convert array to Map
    else No custom URL
        Config->>Config: Use default cycle set
    end
    Config->>Cache: Initialize SWRCache per cycle
    loop For each cycle
        Cache->>Cache: Create independent SWRCache
    end
    
    Note over Client,DB: Runtime: GET /referral-leaderboard?cycle=X
    Client->>API: GET /referral-leaderboard?cycle=cycle-1
    API->>API: Validate cycle parameter
    API->>Cache: Get cache for cycle-1
    alt Cycle not found
        API-->>Client: 404 with valid cycle IDs
    else Cycle cache exists
        Cache->>DB: Query referral data (if stale)
        DB-->>Cache: Return referral data
        Cache->>Cache: Build leaderboard
        Cache-->>API: Return leaderboard or Error
        alt Cache failed
            API-->>Client: 500 error
        else Cache succeeded
            API->>API: Generate page from leaderboard
            API-->>Client: 200 with leaderboard page
        end
    end
    
    Note over Client,DB: Runtime: GET /referral-leaderboard/:referrer
    Client->>API: GET /referral-leaderboard/0x123...
    API->>Cache: Iterate all cycle caches
    loop For each configured cycle
        Cache->>Cache: Read cycle cache
        alt Any cache failed
            API-->>Client: 500 error (fail fast)
        else Cache succeeded
            API->>API: Extract referrer detail
        end
    end
    API->>API: Aggregate all cycle data
    API-->>Client: 200 with all-cycles response
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 237 to 258
@@ -218,11 +258,13 @@ export function deserializeReferrerDetailResponse(
}
Copy link
Contributor

Choose a reason for hiding this comment

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

missing default case for exhaustiveness checking - deserialized could be uninitialized

Suggested change
switch (maybeResponse.responseCode) {
case "ok": {
const data: ReferrerDetailAllCyclesData = {} as ReferrerDetailAllCyclesData;
for (const [cycleId, detail] of Object.entries(maybeResponse.data)) {
// Object.entries only returns existing entries, so detail is never undefined at runtime
data[cycleId as ReferralProgramCycleId] = deserializeReferrerDetail(
detail as SerializedReferrerDetail,
);
}
deserialized = {
responseCode: "ok",
data,
};
break;
}
case "error":
deserialized = maybeResponse;
break;
default: {
const _exhaustiveCheck: never = maybeResponse;
throw new Error(
`Unknown response code: ${(_exhaustiveCheck as SerializedReferrerDetailAllCyclesResponse).responseCode}`,
);
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ens-referrals/src/v1/api/deserialize.ts
Line: 237:258

Comment:
missing default case for exhaustiveness checking - `deserialized` could be uninitialized

```suggestion
  switch (maybeResponse.responseCode) {
    case "ok": {
      const data: ReferrerDetailAllCyclesData = {} as ReferrerDetailAllCyclesData;

      for (const [cycleId, detail] of Object.entries(maybeResponse.data)) {
        // Object.entries only returns existing entries, so detail is never undefined at runtime
        data[cycleId as ReferralProgramCycleId] = deserializeReferrerDetail(
          detail as SerializedReferrerDetail,
        );
      }

      deserialized = {
        responseCode: "ok",
        data,
      };
      break;
    }

    case "error":
      deserialized = maybeResponse;
      break;

    default: {
      const _exhaustiveCheck: never = maybeResponse;
      throw new Error(
        `Unknown response code: ${(_exhaustiveCheck as SerializedReferrerDetailAllCyclesResponse).responseCode}`,
      );
    }
  }
```

How can I resolve this? If you propose a fix, please make it concise.

/**
* URL to the rules for this cycle.
*/
RULES_URL: "https://ensawards.org/ens-holiday-awards-rules",
Copy link
Contributor

Choose a reason for hiding this comment

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

cycle-2 reuses cycle-1's rules URL - update to cycle-specific URL when available

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ens-referrals/src/v1/cycle-defaults.ts
Line: 83:83

Comment:
cycle-2 reuses cycle-1's rules URL - update to cycle-specific URL when available

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 4, 2026

Additional Comments (1)

packages/ens-referrals/src/v1/api/deserialize.ts
missing default case for exhaustiveness checking - deserialized could be uninitialized

  switch (maybeResponse.responseCode) {
    case "ok": {
      deserialized = {
        responseCode: maybeResponse.responseCode,
        data: deserializeReferrerLeaderboardPage(maybeResponse.data),
      } as ReferrerLeaderboardPageResponse;
      break;
    }

    case "error":
      deserialized = maybeResponse;
      break;

    default: {
      const _exhaustiveCheck: never = maybeResponse;
      throw new Error(
        `Unknown response code: ${(_exhaustiveCheck as SerializedReferrerLeaderboardPageResponse).responseCode}`,
      );
    }
  }
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ens-referrals/src/v1/api/deserialize.ts
Line: 197:209

Comment:
missing default case for exhaustiveness checking - `deserialized` could be uninitialized

```suggestion
  switch (maybeResponse.responseCode) {
    case "ok": {
      deserialized = {
        responseCode: maybeResponse.responseCode,
        data: deserializeReferrerLeaderboardPage(maybeResponse.data),
      } as ReferrerLeaderboardPageResponse;
      break;
    }

    case "error":
      deserialized = maybeResponse;
      break;

    default: {
      const _exhaustiveCheck: never = maybeResponse;
      throw new Error(
        `Unknown response code: ${(_exhaustiveCheck as SerializedReferrerLeaderboardPageResponse).responseCode}`,
      );
    }
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Member

@lightwalker-eth lightwalker-eth left a comment

Choose a reason for hiding this comment

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

@Goader Nice work. Reviewed and shared feedback 👍

"@ensnode/ensnode-sdk": patch
---

Introduces referral program cycles support with pre-configured cycle definitions (ENS Holiday Awards December 2025, March 2026 cycle). Updated ENSAnalytics API v1 to support cycle-based leaderboard queries and added cycle configuration to environment schema.
Copy link
Member

Choose a reason for hiding this comment

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

When we create changesets, we should always follow the best practices described here: https://ensnode.io/docs/contributing/prs/#mapping-logical-changes-to-changesets

In other words, the description added to a changeset must apply to each of the apps / packages attached to the changeset. A PR may need multiple changesets to meet this requirement.

Comment on lines 58 to 59
* If CUSTOM_REFERRAL_PROGRAM_CYCLES is set, it should be a URL pointing to
* a JSON file containing custom cycle definitions.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* If CUSTOM_REFERRAL_PROGRAM_CYCLES is set, it should be a URL pointing to
* a JSON file containing custom cycle definitions.
* If CUSTOM_REFERRAL_PROGRAM_CYCLES is set, it should be a URL that returns
* the JSON for a valid serialized custom referral program cycles definition.

Goal: We shouldn't say it has to be a file. It just needs to give us JSON in the right shape.

TODO: Ideally we could reference the specific data type definition for "serialized custom cycle definition" here. I'm still working my way through the PR and haven't gotten to that definition yet.

ENS_HOLIDAY_AWARDS_END?: string;
export interface ReferralProgramCyclesEnvironment {
/**
* Optional URL to a JSON file containing custom referral program cycle definitions.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
* Optional URL to a JSON file containing custom referral program cycle definitions.
* Optional URL that returns the JSON for a valid serialized custom referral program cycles definition.

# Note: ENS_HOLIDAY_AWARDS_START date must be before or the same as ENS_HOLIDAY_AWARDS_END
# ENS_HOLIDAY_AWARDS_START="2025-12-01T00:00:00Z"
# ENS_HOLIDAY_AWARDS_END="2025-12-31T23:59:59Z"
# Custom Referral Program Cycles (optional)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# Custom Referral Program Cycles (optional)
# Custom Referral Program Cycle Set Definition (optional)

# ENS_HOLIDAY_AWARDS_START="2025-12-01T00:00:00Z"
# ENS_HOLIDAY_AWARDS_END="2025-12-31T23:59:59Z"
# Custom Referral Program Cycles (optional)
# URL to a JSON file containing custom referral program cycle definitions.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
# URL to a JSON file containing custom referral program cycle definitions.
# URL that returns the JSON for a valid serialized custom referral program cycle set definition.

* if (response.responseCode === ReferrerDetailResponseCodes.Ok) {
* if (response.data.type === ReferrerDetailTypeIds.Ranked) {
* if (response.responseCode === ReferrerDetailAllCyclesResponseCodes.Ok) {
* // If "cycle-1" is configured, it will be in response.data
Copy link
Member

Choose a reason for hiding this comment

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

Please see related comments warning about a design for this API where the set of cycle ids is assumed to always include some predefined set. I believe we should actively work to break such an assumption from being made for change management purposes.

* Individual cycle caches maintain their own stale-while-revalidate behavior, so a previously
* successfully fetched cycle continues serving its data even if a subsequent refresh fails.
*/
referralLeaderboardCyclesCaches: ReferralLeaderboardCyclesCacheMap;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
referralLeaderboardCyclesCaches: ReferralLeaderboardCyclesCacheMap;
referralLeaderboardCyclesCaches: ReferralLeaderboardCyclesCacheMap | Error;

I believe we need to do this? Ex: If we haven't been able to fetch a custom set of referral program configs yet?

cycleId: ReferralProgramCycleId,
): () => Promise<ReferrerLeaderboard> {
return async (): Promise<ReferrerLeaderboard> => {
const cycle = config.referralProgramCycleSet.get(cycleId) as ReferralProgramCycle | undefined;
Copy link
Member

Choose a reason for hiding this comment

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

I believe no need for the "as ..." here?

*
* @returns A map from cycle ID to its dedicated SWRCache
*/
function initializeCyclesCaches(): ReferralLeaderboardCyclesCacheMap {
Copy link
Member

Choose a reason for hiding this comment

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

Please see my related comments for how I believe we need to support the possibility of building this lazily / asynchronously after app startup without blocking app startup.

@@ -117,66 +155,70 @@ const referrerAddressSchema = z.object({
referrer: makeLowercaseAddressSchema("Referrer address").describe("Referrer Ethereum address"),
});

Copy link
Member

Choose a reason for hiding this comment

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

In other comments I shared feedback on how the config should at most hold the URL for a custom referral program config set, and should not hold the referral program config set itself.

Ideally clients would have a method to fetch the referral program config set the server is using. Could you introduce a new API for this and also add a convenient helper method for it in the client?

type ReferralLeaderboardCyclesCacheMap,
} from "@/cache/referral-leaderboard-cycles.cache";
import { factory } from "@/lib/hono-factory";
import { referralProgramCycleConfigSetMiddleware } from "@/middleware/referral-program-cycle-set.middleware";
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/ensnode-sdk/src/shared/cache/swr-cache.ts (1)

125-133: 🧹 Nitpick | 🔵 Trivial

Stale comment: no longer limited to "first revalidation".

Line 126 says "only update the cache if this is the first revalidation", but the condition on Line 127 now also updates when the previous cache holds an Error. Consider updating the comment to reflect the new semantics (e.g., "update the cache if no prior cache exists or the prior result was also an Error").

📝 Suggested comment update
-          // on error, only update the cache if this is the first revalidation
+          // on error, update the cache if there is no prior cache or the prior result was also an Error
           if (!this.cache || this.cache.result instanceof Error) {
packages/ens-referrals/src/v1/rules.ts (1)

45-73: 🧹 Nitpick | 🔵 Trivial

validateReferralProgramRules does not validate rulesUrl.

The function validates all other fields but skips rulesUrl. Since the field is typed as URL, TypeScript enforces it at compile time, but deserialization paths could pass invalid values at runtime. Consider adding a runtime check (e.g., if (!(rules.rulesUrl instanceof URL))) for consistency with the other field validations.

🛡️ Proposed validation addition
   if (rules.endTime < rules.startTime) {
     throw new Error(
       `ReferralProgramRules: startTime: ${rules.startTime} is after endTime: ${rules.endTime}.`,
     );
   }
+
+  if (!(rules.rulesUrl instanceof URL)) {
+    throw new Error(`ReferralProgramRules: rulesUrl must be a URL instance.`);
+  }
 };
apps/ensapi/src/config/config.schema.test.ts (1)

58-84: 🧹 Nitpick | 🔵 Trivial

Consider adding a test case for a valid customReferralProgramCycleConfigSetUrl.

All test configs set customReferralProgramCycleConfigSetUrl: undefined. There's no coverage for when the env var is set to a valid URL, which would verify the URL parsing/validation path in buildConfigFromEnvironment.

packages/ens-referrals/src/v1/api/serialize.ts (1)

187-200: 🧹 Nitpick | 🔵 Trivial

serializeReferrerLeaderboardPageResponse lacks an exhaustive check, unlike the other serializers.

The other two response serializers (serializeReferrerDetailCyclesResponse, serializeReferralProgramCycleConfigSetResponse) both have default exhaustiveness checks. This one doesn't, creating an inconsistency. Consider adding one for consistency.

♻️ Proposed fix
     case ReferrerLeaderboardPageResponseCodes.Error:
       return response;
+
+    default: {
+      const _exhaustiveCheck: never = response;
+      throw new Error(
+        `Unknown response code: ${(_exhaustiveCheck as ReferrerLeaderboardPageResponse).responseCode}`,
+      );
+    }
   }
🤖 Fix all issues with AI agents
In `@apps/ensapi/src/cache/referral-leaderboard-cycles.cache.ts`:
- Around line 126-151: The module-level cachedInstance is never cleared which
prevents reinitializing different configurations in tests; add and export a
resetReferralLeaderboardCyclesCaches() function that (1) iterates existing
cachedInstance values and calls a clear/teardown method on each SWRCache if
available (or safely ignore if not), and (2) sets cachedInstance = null so
initializeReferralLeaderboardCyclesCaches(cycleConfigSet) can recreate caches;
export this reset function for test usage and keep
initializeReferralLeaderboardCyclesCaches implementation unchanged otherwise.
- Around line 88-94: The current info-level log in
referral-leaderboard-cycles.cache.ts that calls logger.info with
serializeReferralProgramRules(cycleConfig.rules) for cycleSlug is too verbose
for a rebuild every 2 minutes; change it to logger.debug to lower verbosity, or
implement a one-time info log by tracking first build per cycle (e.g., a Set or
Map like seenCycles in the ReferralLeaderboardCyclesCache class) and only log
the full serialized rules via logger.info on the first build for that cycle,
otherwise emit the shorter debug-level message.

In `@apps/ensapi/src/cache/referral-program-cycle-set.cache.ts`:
- Around line 59-77: The SWR wrapper referralProgramCycleConfigSetCache is
duplicating success logs since loadReferralProgramCycleConfigSet already logs
successful loads; remove the success logger.info call inside the wrapper's fn
(the "Referral program cycle config set cached successfully" log) and leave the
async call and the existing catch block intact so errors are still logged but
success logging is only emitted by loadReferralProgramCycleConfigSet.

In `@apps/ensapi/src/config/config.schema.ts`:
- Around line 48-61: Update the CustomReferralProgramCycleConfigSetUrlSchema
transform to use the string literal "custom" for the issue code instead of
z.ZodIssueCode.custom: locate the transform callback inside
CustomReferralProgramCycleConfigSetUrlSchema and replace code:
z.ZodIssueCode.custom with code: "custom" while keeping the existing message,
ctx.addIssue shape, return z.NEVER and .optional() behavior unchanged to match
the project's Zod v4 pattern.

In `@apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts`:
- Around line 57-86: Extract the repeated mock setup into a helper (e.g.,
setupCycleMocks) that accepts an array of cycles and internally builds the
Map<ReferralProgramCycleSlug, SWRCache<ReferrerLeaderboard>> and the cycle
config Map, then installs the two mocked middlewares
(cycleSetMiddleware.referralProgramCycleConfigSetMiddleware and
cyclesCachesMiddleware.referralLeaderboardCyclesCachesMiddleware) to set those
maps on the context; replace the repeated blocks in tests (the inline
mockCyclesCaches creation and vi.mocked(...).mockImplementation calls) with
calls to this helper, keeping types like SWRCache and ReferrerLeaderboard
consistent with the existing tests.
- Around line 769-857: The test's mock cycle timestamps don't match their
slugs/displayNames; update the start/end timestamps in the mockCycleConfigSet
entries so each buildReferralProgramRules call uses the correct epoch seconds
for the slug's month (e.g., for slug "2025-12" use start=2025-12-01 and
end=2025-12-31, for "2026-03" use 2026-03-01/2026-03-31, and for "2026-06" use
2026-06-01/2026-06-30) inside the Map used by the
referralProgramCycleConfigSetMiddleware (the Map keys "2025-12", "2026-03",
"2026-06" and their buildReferralProgramRules calls are the unique symbols to
edit).

In `@apps/ensapi/src/handlers/ensanalytics-api-v1.ts`:
- Around line 290-305: The filter that creates validCycleLeaderboards is only
doing TypeScript narrowing but is redundant because after the earlier early
return all entries in cycleLeaderboards are non-Error; remove the .filter(...)
step and directly map cycleLeaderboards to build cyclesData, using an explicit
type assertion (e.g. cast cycleLeaderboards to Array<{ cycleSlug:
ReferralProgramCycleSlug; leaderboard: ReferrerLeaderboard }> or use a non-null
assertion within the map) so getReferrerDetail(referrer, leaderboard) compiles
and the result is cast to ReferrerDetailCyclesData.

In `@apps/ensapi/src/middleware/referral-leaderboard-cycles-caches.middleware.ts`:
- Line 6: The import referralProgramCycleConfigSetMiddleware is not used at
runtime (only in a JSDoc `@link`), so remove the runtime import and either change
it to a type-only import (import type { referralProgramCycleConfigSetMiddleware
} ...) or delete the import entirely and update the JSDoc to reference the
middleware by name as plain text (e.g.,
"referralProgramCycleConfigSetMiddleware") instead of using {`@link`}; update the
JSDoc on the middleware function that mentions
referralProgramCycleConfigSetMiddleware accordingly.

In `@packages/ens-referrals/src/v1/api/zod-schemas.ts`:
- Around line 222-238: The referenced slug schema factory
makeReferralProgramCycleSlugSchema is declared after schemas that use it
(makeReferrerDetailCyclesArraySchema, makeReferrerDetailCyclesResponseOkSchema),
harming readability; move the entire makeReferralProgramCycleSlugSchema
declaration so it appears before any schemas that reference it (e.g., place it
above makeReferrerDetailSchema and before
makeReferrerDetailCyclesArraySchema/makeReferrerDetailCyclesResponseOkSchema) so
dependent factories reference a prior, clearly defined helper.
- Around line 340-348: The makeReferralProgramCycleConfigSetDataSchema currently
builds an object with cycles: z.array(makeReferralProgramCycleConfigSchema(...))
but omits the duplicate-slug refinement present in
makeReferralProgramCycleConfigSetArraySchema; update
makeReferralProgramCycleConfigSetDataSchema to include the same .refine(...)
that checks for duplicate cycle.slug (or refactor to call/reuse
makeReferralProgramCycleConfigSetArraySchema for the cycles validation) so both
schemas enforce unique slugs for entries produced by
makeReferralProgramCycleConfigSchema.

In `@packages/ens-referrals/src/v1/cycle-defaults.ts`:
- Around line 39-50: The rulesUrl passed into cycle2 (the last argument to
buildReferralProgramRules in the cycle2 object) currently points to the ENS
Holiday Awards page; either replace new
URL("https://ensawards.org/ens-holiday-awards-rules") with the correct March
2026 rules URL when available, or add a concise inline comment next to the
rulesUrl argument noting this is a temporary placeholder until the dedicated
March 2026 rules page is published so reviewers know it is intentional.
- Around line 26-55: The slug strings are duplicated and can drift; create the
cycle objects (e.g., cycle1 and cycle2 of type ReferralProgramCycleConfig) first
and then build the Map using each object's slug (e.g., use cycle1.slug and
cycle2.slug as keys) instead of hardcoding "2025-12"/"2026-03" so the Map keys
always reflect the ReferralProgramCycleConfig.slug values.

In `@packages/ensnode-sdk/src/shared/datetime.ts`:
- Around line 35-43: parseTimestamp currently uses new Date(isoDateString) which
treats timezone-naive ISO strings inconsistently; update parseTimestamp to
validate that isoDateString contains an explicit timezone designator (a trailing
'Z' or an offset like +HH:MM/-HH:MM) before constructing Date, and throw a
descriptive Error if missing (e.g., "Timezone required: provide Z or offset") so
callers must supply unambiguous timestamps; keep the rest of the logic
(date.getTime() check and getUnixTime cast) intact and reference parseTimestamp
and getUnixTime when making the change.

Comment on lines +88 to +94
logger.info(
`Building referrer leaderboard for ${cycleSlug} with rules:\n${JSON.stringify(
serializeReferralProgramRules(cycleConfig.rules),
null,
2,
)}`,
);
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider reducing log verbosity for rules output.

Logging the full serialized rules at info level on every cache rebuild (which happens every 2 minutes per cycle) will produce substantial log volume. Consider using debug level instead, or logging rules only on the first build.

🤖 Prompt for AI Agents
In `@apps/ensapi/src/cache/referral-leaderboard-cycles.cache.ts` around lines 88 -
94, The current info-level log in referral-leaderboard-cycles.cache.ts that
calls logger.info with serializeReferralProgramRules(cycleConfig.rules) for
cycleSlug is too verbose for a rebuild every 2 minutes; change it to
logger.debug to lower verbosity, or implement a one-time info log by tracking
first build per cycle (e.g., a Set or Map like seenCycles in the
ReferralLeaderboardCyclesCache class) and only log the full serialized rules via
logger.info on the first build for that cycle, otherwise emit the shorter
debug-level message.

Comment on lines +126 to +151
export function initializeReferralLeaderboardCyclesCaches(
cycleConfigSet: ReferralProgramCycleConfigSet,
): ReferralLeaderboardCyclesCacheMap {
// Return cached instance if already initialized
if (cachedInstance !== null) {
return cachedInstance;
}

const caches: ReferralLeaderboardCyclesCacheMap = new Map();

for (const [cycleSlug, cycleConfig] of cycleConfigSet) {
const cache = new SWRCache({
fn: createCycleLeaderboardBuilder(cycleConfig),
ttl: minutesToSeconds(1),
proactiveRevalidationInterval: minutesToSeconds(2),
proactivelyInitialize: true,
});

caches.set(cycleSlug, cache);
logger.info(`Initialized leaderboard cache for ${cycleSlug}`);
}

// Cache the instance for subsequent calls
cachedInstance = caches;
return caches;
}
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Singleton has no reset mechanism for testing.

The module-level cachedInstance can never be cleared once set. This makes integration tests that need different cycle configurations across test cases difficult to set up. Consider exposing a resetReferralLeaderboardCyclesCaches() for test use, or accepting that tests will mock at the middleware level (as the test file does).

🤖 Prompt for AI Agents
In `@apps/ensapi/src/cache/referral-leaderboard-cycles.cache.ts` around lines 126
- 151, The module-level cachedInstance is never cleared which prevents
reinitializing different configurations in tests; add and export a
resetReferralLeaderboardCyclesCaches() function that (1) iterates existing
cachedInstance values and calls a clear/teardown method on each SWRCache if
available (or safely ignore if not), and (2) sets cachedInstance = null so
initializeReferralLeaderboardCyclesCaches(cycleConfigSet) can recreate caches;
export this reset function for test usage and keep
initializeReferralLeaderboardCyclesCaches implementation unchanged otherwise.

Comment on lines +59 to +77
export const referralProgramCycleConfigSetCache = new SWRCache<ReferralProgramCycleConfigSet>({
fn: async () => {
try {
const cycleConfigSet = await loadReferralProgramCycleConfigSet();
logger.info("Referral program cycle config set cached successfully");
return cycleConfigSet;
} catch (error) {
logger.error(
error,
"Error occurred while loading referral program cycle config set. The cache will remain empty.",
);
throw error;
}
},
ttl: Number.POSITIVE_INFINITY,
errorTtl: minutesToSeconds(1),
proactiveRevalidationInterval: undefined,
proactivelyInitialize: true,
});
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Redundant success logging between loader and cache wrapper.

loadReferralProgramCycleConfigSet already logs "Successfully loaded N cycles" (lines 29, 44), and the SWR fn wrapper logs "cached successfully" (line 63). On the success path, this produces three info-level log lines for a single load operation. Consider removing the duplicate logging in the SWR wrapper to reduce noise.

♻️ Simplify the SWR fn wrapper
 export const referralProgramCycleConfigSetCache = new SWRCache<ReferralProgramCycleConfigSet>({
   fn: async () => {
-    try {
-      const cycleConfigSet = await loadReferralProgramCycleConfigSet();
-      logger.info("Referral program cycle config set cached successfully");
-      return cycleConfigSet;
-    } catch (error) {
-      logger.error(
-        error,
-        "Error occurred while loading referral program cycle config set. The cache will remain empty.",
-      );
-      throw error;
-    }
+    return loadReferralProgramCycleConfigSet();
   },

The SWR cache already handles error state internally. If you need the error log, keep only the catch block but drop the success log.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const referralProgramCycleConfigSetCache = new SWRCache<ReferralProgramCycleConfigSet>({
fn: async () => {
try {
const cycleConfigSet = await loadReferralProgramCycleConfigSet();
logger.info("Referral program cycle config set cached successfully");
return cycleConfigSet;
} catch (error) {
logger.error(
error,
"Error occurred while loading referral program cycle config set. The cache will remain empty.",
);
throw error;
}
},
ttl: Number.POSITIVE_INFINITY,
errorTtl: minutesToSeconds(1),
proactiveRevalidationInterval: undefined,
proactivelyInitialize: true,
});
export const referralProgramCycleConfigSetCache = new SWRCache<ReferralProgramCycleConfigSet>({
fn: async () => {
return loadReferralProgramCycleConfigSet();
},
ttl: Number.POSITIVE_INFINITY,
errorTtl: minutesToSeconds(1),
proactiveRevalidationInterval: undefined,
proactivelyInitialize: true,
});
🤖 Prompt for AI Agents
In `@apps/ensapi/src/cache/referral-program-cycle-set.cache.ts` around lines 59 -
77, The SWR wrapper referralProgramCycleConfigSetCache is duplicating success
logs since loadReferralProgramCycleConfigSet already logs successful loads;
remove the success logger.info call inside the wrapper's fn (the "Referral
program cycle config set cached successfully" log) and leave the async call and
the existing catch block intact so errors are still logged but success logging
is only emitted by loadReferralProgramCycleConfigSet.

Comment on lines +48 to +61
const CustomReferralProgramCycleConfigSetUrlSchema = z
.string()
.pipe(makeDatetimeSchema())
.transform((date) => getUnixTime(date));
.transform((val, ctx) => {
try {
return new URL(val);
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `CUSTOM_REFERRAL_PROGRAM_CYCLES is not a valid URL: ${val}`,
});
return z.NEVER;
}
})
.optional();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find the package.json to check Zod version
fd package.json | head -5 | xargs grep -l "zod"

Repository: namehash/ensnode

Length of output: 113


🏁 Script executed:

# Check the config.schema.ts imports and full context
cat -n apps/ensapi/src/config/config.schema.ts | head -70

Repository: namehash/ensnode

Length of output: 2690


🏁 Script executed:

# Find the validations.ts files mentioned in the review
fd validations.ts

Repository: namehash/ensnode

Length of output: 335


🏁 Script executed:

# Search for ZodIssueCode usage across the codebase
rg "ZodIssueCode|code:\s*['\"]custom['\"]" --type ts

Repository: namehash/ensnode

Length of output: 4464


🏁 Script executed:

# Check one of the validations.ts files to see the exact pattern used
cat -n apps/ensapi/src/config/validations.ts | head -80

Repository: namehash/ensnode

Length of output: 2184


🏁 Script executed:

# Check Zod version in the root package.json
cat -n package.json | grep -A 5 -B 5 "zod"

Repository: namehash/ensnode

Length of output: 42


🏁 Script executed:

# Check if z.ZodIssueCode exists in the zod/v4 import
rg "ZodIssueCode" apps/ensapi/src/config/config.schema.ts -B 5 -A 5

Repository: namehash/ensnode

Length of output: 318


🌐 Web query:

Zod v4 z.ZodIssueCode API issue codes documentation

💡 Result:

In Zod v4, the authoritative “issue code” list is defined in the v4 core error types (not as a separately maintained docs page).

Where the v4 issue codes are defined

  • Core (source of truth): src/v4/core/errors.ts defines $ZodIssue and $ZodIssueCode = $ZodIssue["code"]. (app.unpkg.com)
  • Classic compatibility export: v4/classic/compat.d.cts exports z.ZodIssueCode, but it’s deprecated in favor of using the raw string-literal codes directly. (app.unpkg.com)

Zod v4 issue codes (core)

From $ZodIssue in errors.ts, the v4 codes are: (app.unpkg.com)

  • "invalid_type"
  • "too_big"
  • "too_small"
  • "invalid_format"
  • "not_multiple_of"
  • "unrecognized_keys"
  • "invalid_union"
  • "invalid_key"
  • "invalid_element"
  • "invalid_value"
  • "custom"

Practical usage

import * as z from "zod";

function handle(issue: z.$ZodIssue) {
  if (issue.code === "invalid_type") {
    // ...
  }
}

(If you’re using the classic compat layer, z.ZodIssueCode exists but is deprecated.) (app.unpkg.com)

Citations:


Use string literal "custom" instead of z.ZodIssueCode.custom for consistency with Zod v4 patterns.

The codebase uses code: "custom" as a string literal in all other validation schemas (45+ instances across the codebase). Here, z.ZodIssueCode.custom relies on Zod v4's deprecated compat layer. For consistency and alignment with Zod v4 best practices, use the string literal instead:

♻️ Proposed fix
       ctx.addIssue({
-        code: z.ZodIssueCode.custom,
+        code: "custom",
         message: `CUSTOM_REFERRAL_PROGRAM_CYCLES is not a valid URL: ${val}`,
       });
🤖 Prompt for AI Agents
In `@apps/ensapi/src/config/config.schema.ts` around lines 48 - 61, Update the
CustomReferralProgramCycleConfigSetUrlSchema transform to use the string literal
"custom" for the issue code instead of z.ZodIssueCode.custom: locate the
transform callback inside CustomReferralProgramCycleConfigSetUrlSchema and
replace code: z.ZodIssueCode.custom with code: "custom" while keeping the
existing message, ctx.addIssue shape, return z.NEVER and .optional() behavior
unchanged to match the project's Zod v4 pattern.

Comment on lines 57 to 86
it("returns requested records when referrer leaderboard has multiple pages of data", async () => {
// Arrange: set `referrerLeaderboardV1` context var
vi.mocked(middleware.referrerLeaderboardMiddlewareV1).mockImplementation(async (c, next) => {
c.set("referrerLeaderboardV1", populatedReferrerLeaderboard);
// Arrange: mock cache map with 2025-12
const mockCyclesCaches = new Map<ReferralProgramCycleSlug, SWRCache<ReferrerLeaderboard>>([
[
"2025-12",
{
read: async () => populatedReferrerLeaderboard,
} as SWRCache<ReferrerLeaderboard>,
],
]);

// Mock cycle set middleware to provide a mock cycle set
const mockCycleConfigSet = new Map([
["2025-12", { slug: "2025-12", displayName: "Cycle 1", rules: {} as any }],
]);
vi.mocked(cycleSetMiddleware.referralProgramCycleConfigSetMiddleware).mockImplementation(
async (c, next) => {
c.set("referralProgramCycleConfigSet", mockCycleConfigSet);
return await next();
},
);

// Mock caches middleware to provide the mock caches
vi.mocked(
cyclesCachesMiddleware.referralLeaderboardCyclesCachesMiddleware,
).mockImplementation(async (c, next) => {
c.set("referralLeaderboardCyclesCaches", mockCyclesCaches);
return await next();
});

Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider extracting the repetitive mock setup into a helper.

The mock setup pattern (create cache map, mock cycle config set middleware, mock caches middleware) is repeated in nearly every test. A helper function would reduce ~20 lines of boilerplate per test.

♻️ Example helper
function setupCycleMocks(
  cycles: Array<{ slug: string; displayName: string; leaderboard: ReferrerLeaderboard | Error }>,
) {
  const mockCyclesCaches = new Map(
    cycles.map(({ slug, leaderboard }) => [
      slug,
      { read: async () => leaderboard } as SWRCache<ReferrerLeaderboard>,
    ]),
  );
  const mockCycleConfigSet = new Map(
    cycles.map(({ slug, displayName }) => [
      slug,
      { slug, displayName, rules: {} as any },
    ]),
  );
  vi.mocked(cycleSetMiddleware.referralProgramCycleConfigSetMiddleware).mockImplementation(
    async (c, next) => {
      c.set("referralProgramCycleConfigSet", mockCycleConfigSet);
      return await next();
    },
  );
  vi.mocked(cyclesCachesMiddleware.referralLeaderboardCyclesCachesMiddleware).mockImplementation(
    async (c, next) => {
      c.set("referralLeaderboardCyclesCaches", mockCyclesCaches);
      return await next();
    },
  );
}
🤖 Prompt for AI Agents
In `@apps/ensapi/src/handlers/ensanalytics-api-v1.test.ts` around lines 57 - 86,
Extract the repeated mock setup into a helper (e.g., setupCycleMocks) that
accepts an array of cycles and internally builds the
Map<ReferralProgramCycleSlug, SWRCache<ReferrerLeaderboard>> and the cycle
config Map, then installs the two mocked middlewares
(cycleSetMiddleware.referralProgramCycleConfigSetMiddleware and
cyclesCachesMiddleware.referralLeaderboardCyclesCachesMiddleware) to set those
maps on the context; replace the repeated blocks in tests (the inline
mockCyclesCaches creation and vi.mocked(...).mockImplementation calls) with
calls to this helper, keeping types like SWRCache and ReferrerLeaderboard
consistent with the existing tests.

Comment on lines +222 to +238
export const makeReferrerDetailCyclesArraySchema = (
valueLabel: string = "ReferrerDetailCyclesArray",
) =>
z
.array(makeReferralProgramCycleSlugSchema(`${valueLabel}[cycle]`))
.min(1, `${valueLabel} must contain at least 1 cycle`)
.max(
MAX_CYCLES_PER_REQUEST,
`${valueLabel} must not contain more than ${MAX_CYCLES_PER_REQUEST} cycles`,
)
.refine(
(cycles) => {
const uniqueCycles = new Set(cycles);
return uniqueCycles.size === cycles.length;
},
{ message: `${valueLabel} must not contain duplicate cycle slugs` },
);
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

makeReferralProgramCycleSlugSchema is referenced before its declaration.

makeReferrerDetailCyclesArraySchema (line 226) and makeReferrerDetailCyclesResponseOkSchema (line 260) both call makeReferralProgramCycleSlugSchema, which isn't defined until line 296. This works at runtime because these are factory functions invoked lazily (after module initialization completes), so no TDZ error occurs. However, it hurts readability — the slug schema is a foundational building block and would be clearer if defined before its dependents.

♻️ Suggested reorder

Move the makeReferralProgramCycleSlugSchema definition (lines 288–305) above makeReferrerDetailSchema (line 213), so it precedes all schemas that depend on it.

🤖 Prompt for AI Agents
In `@packages/ens-referrals/src/v1/api/zod-schemas.ts` around lines 222 - 238, The
referenced slug schema factory makeReferralProgramCycleSlugSchema is declared
after schemas that use it (makeReferrerDetailCyclesArraySchema,
makeReferrerDetailCyclesResponseOkSchema), harming readability; move the entire
makeReferralProgramCycleSlugSchema declaration so it appears before any schemas
that reference it (e.g., place it above makeReferrerDetailSchema and before
makeReferrerDetailCyclesArraySchema/makeReferrerDetailCyclesResponseOkSchema) so
dependent factories reference a prior, clearly defined helper.

Comment on lines +340 to +348
/**
* Schema for {@link ReferralProgramCycleConfigSetData}.
*/
export const makeReferralProgramCycleConfigSetDataSchema = (
valueLabel: string = "ReferralProgramCycleConfigSetData",
) =>
z.object({
cycles: z.array(makeReferralProgramCycleConfigSchema(`${valueLabel}.cycles[cycle]`)),
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check where makeReferralProgramCycleConfigSetDataSchema is used
rg -n 'makeReferralProgramCycleConfigSetDataSchema' --type=ts -C2

Repository: namehash/ensnode

Length of output: 952


🏁 Script executed:

#!/bin/bash
# Look at the comparison schema (makeReferralProgramCycleConfigSetArraySchema) to see if it has the refinement
rg -n 'makeReferralProgramCycleConfigSetArraySchema' --type=ts -C5

Repository: namehash/ensnode

Length of output: 2896


🏁 Script executed:

#!/bin/bash
# Get more context on the zod-schemas.ts file around both functions
cat -n packages/ens-referrals/src/v1/api/zod-schemas.ts | sed -n '320,380p'

Repository: namehash/ensnode

Length of output: 2506


makeReferralProgramCycleConfigSetDataSchema lacks the duplicate-slug refinement present in makeReferralProgramCycleConfigSetArraySchema.

The schema validates the cycles array within a response object but omits the .refine() check for duplicate slugs (lines 328–338 in makeReferralProgramCycleConfigSetArraySchema). For consistency and defensive validation, apply the same duplicate-slug refinement here or refactor to reuse makeReferralProgramCycleConfigSetArraySchema.

🤖 Prompt for AI Agents
In `@packages/ens-referrals/src/v1/api/zod-schemas.ts` around lines 340 - 348, The
makeReferralProgramCycleConfigSetDataSchema currently builds an object with
cycles: z.array(makeReferralProgramCycleConfigSchema(...)) but omits the
duplicate-slug refinement present in
makeReferralProgramCycleConfigSetArraySchema; update
makeReferralProgramCycleConfigSetDataSchema to include the same .refine(...)
that checks for duplicate cycle.slug (or refactor to call/reuse
makeReferralProgramCycleConfigSetArraySchema for the cycles validation) so both
schemas enforce unique slugs for entries produced by
makeReferralProgramCycleConfigSchema.

Comment on lines +26 to +55
const cycle1: ReferralProgramCycleConfig = {
slug: "2025-12",
displayName: "ENS Holiday Awards",
rules: buildReferralProgramRules(
parseUsdc("10000"),
10,
parseTimestamp("2025-12-01T00:00:00Z"),
parseTimestamp("2025-12-31T23:59:59Z"),
subregistryId,
new URL("https://ensawards.org/ens-holiday-awards-rules"),
),
};

const cycle2: ReferralProgramCycleConfig = {
slug: "2026-03",
displayName: "March 2026",
rules: buildReferralProgramRules(
parseUsdc("10000"),
10,
parseTimestamp("2026-03-01T00:00:00Z"),
parseTimestamp("2026-03-31T23:59:59Z"),
subregistryId,
new URL("https://ensawards.org/ens-holiday-awards-rules"),
),
};

return new Map([
["2025-12", cycle1],
["2026-03", cycle2],
]);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Slug strings are duplicated between config objects and Map keys — risk of silent mismatch.

The slug values appear both in the ReferralProgramCycleConfig.slug field (lines 27, 40) and as the Map keys (lines 53, 54). If one is changed without updating the other, the Map key and config slug would silently diverge.

♻️ Proposed fix: derive Map keys from config objects
-  return new Map([
-    ["2025-12", cycle1],
-    ["2026-03", cycle2],
-  ]);
+  return new Map([
+    [cycle1.slug, cycle1],
+    [cycle2.slug, cycle2],
+  ]);
🤖 Prompt for AI Agents
In `@packages/ens-referrals/src/v1/cycle-defaults.ts` around lines 26 - 55, The
slug strings are duplicated and can drift; create the cycle objects (e.g.,
cycle1 and cycle2 of type ReferralProgramCycleConfig) first and then build the
Map using each object's slug (e.g., use cycle1.slug and cycle2.slug as keys)
instead of hardcoding "2025-12"/"2026-03" so the Map keys always reflect the
ReferralProgramCycleConfig.slug values.

Comment on lines +39 to +50
const cycle2: ReferralProgramCycleConfig = {
slug: "2026-03",
displayName: "March 2026",
rules: buildReferralProgramRules(
parseUsdc("10000"),
10,
parseTimestamp("2026-03-01T00:00:00Z"),
parseTimestamp("2026-03-31T23:59:59Z"),
subregistryId,
new URL("https://ensawards.org/ens-holiday-awards-rules"),
),
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Cycle 2 rulesUrl points to the ENS Holiday Awards rules page.

Line 48 uses https://ensawards.org/ens-holiday-awards-rules for the March 2026 cycle. If this is intentional (as a placeholder until a dedicated March 2026 rules page exists), consider adding a comment. Otherwise, update to the correct URL when available.

🤖 Prompt for AI Agents
In `@packages/ens-referrals/src/v1/cycle-defaults.ts` around lines 39 - 50, The
rulesUrl passed into cycle2 (the last argument to buildReferralProgramRules in
the cycle2 object) currently points to the ENS Holiday Awards page; either
replace new URL("https://ensawards.org/ens-holiday-awards-rules") with the
correct March 2026 rules URL when available, or add a concise inline comment
next to the rulesUrl argument noting this is a temporary placeholder until the
dedicated March 2026 rules page is published so reviewers know it is
intentional.

Comment on lines +35 to +43
export function parseTimestamp(isoDateString: string): UnixTimestamp {
const date = new Date(isoDateString);

if (Number.isNaN(date.getTime())) {
throw new Error(`Invalid date string: ${isoDateString}`);
}

return getUnixTime(date) as UnixTimestamp;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Timezone-naive strings silently produce local-time results.

new Date("2025-12-01T00:00:00") (no Z or offset) is parsed as local time per the JS spec, while new Date("2025-12-01") (date-only) is parsed as UTC. This inconsistency could cause subtle bugs if callers omit the timezone designator. Consider either documenting this caveat more explicitly or validating that the input contains a timezone offset.

🤖 Prompt for AI Agents
In `@packages/ensnode-sdk/src/shared/datetime.ts` around lines 35 - 43,
parseTimestamp currently uses new Date(isoDateString) which treats
timezone-naive ISO strings inconsistently; update parseTimestamp to validate
that isoDateString contains an explicit timezone designator (a trailing 'Z' or
an offset like +HH:MM/-HH:MM) before constructing Date, and throw a descriptive
Error if missing (e.g., "Timezone required: provide Z or offset") so callers
must supply unambiguous timestamps; keep the rest of the logic (date.getTime()
check and getUnixTime cast) intact and reference parseTimestamp and getUnixTime
when making the change.

Copy link
Contributor

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

Copilot reviewed 35 out of 35 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +35 to +43
export function parseTimestamp(isoDateString: string): UnixTimestamp {
const date = new Date(isoDateString);

if (Number.isNaN(date.getTime())) {
throw new Error(`Invalid date string: ${isoDateString}`);
}

return getUnixTime(date) as UnixTimestamp;
}
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

parseTimestamp() returns getUnixTime(date) as UnixTimestamp, which bypasses the existing deserializeUnixTimestamp validation used elsewhere in this module. To keep invariants consistent (and avoid accidental non-integer/invalid timestamps being cast through), return deserializeUnixTimestamp(getUnixTime(date), "UnixTimestamp") instead of a type assertion.

Copilot uses AI. Check for mistakes.
makeUrlSchema,
} from "@ensnode/ensnode-sdk/internal";

import type { ReferralProgramCycleSlug } from "../cycle";
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

ReferralProgramCycleSlug is imported as a type but never used in this file (JSDoc references don't count). With Biome's recommended rules, this is likely to fail lint due to an unused import; please remove it.

Suggested change
import type { ReferralProgramCycleSlug } from "../cycle";

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +17
* @example "2025-12" // December 2025 cycle
* @example "2026-03" // March 2026 cycle
* @example "holiday-special" // Custom named cycle
*/
export type ReferralProgramCycleSlug = string;
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

The implementation introduces ReferralProgramCycleSlug values like "2025-12"/"2026-03", but the PR description/issue #1522 calls for enum-like cycle IDs (cycle-1, cycle-2) with an extensible ReferralProgramCycleId typing strategy. Either align the code to the documented ID strategy (including defaults) or update the PR description/issue plan to reflect the date-based slug approach so downstream consumers aren’t built against the wrong identifier format.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +61
/**
* Schema for validating custom referral program cycle config set URL.
*/
const CustomReferralProgramCycleConfigSetUrlSchema = z
.string()
.pipe(makeDatetimeSchema())
.transform((date) => getUnixTime(date));
.transform((val, ctx) => {
try {
return new URL(val);
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `CUSTOM_REFERRAL_PROGRAM_CYCLES is not a valid URL: ${val}`,
});
return z.NEVER;
}
})
.optional();
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

PR description calls out a configuration loading strategy in apps/ensapi/src/config/config.schema.ts (a loadReferralProgramCycleSet() that fetches + validates JSON). In this diff, config.schema.ts only validates/parses the URL, while the fetch/deserialize logic lives in src/cache/referral-program-cycle-set.cache.ts. Please either move the loading logic into config.schema.ts as described or update the PR description to match the actual architecture (to avoid reviewer/maintainer confusion).

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +132
# * API requests receive error responses until successful load
# - Requests received before initial load completes will receive error responses
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

The docs claim “Requests received before initial load completes will receive error responses”, but SWRCache.read() awaits the initial revalidate() when cache is empty. That means requests will block until the cycle config set load finishes (success or failure), not immediately return an error just because initialization is in progress. Please update this comment (or change the cache/middleware behavior) so it reflects what actually happens.

Suggested change
# * API requests receive error responses until successful load
# - Requests received before initial load completes will receive error responses
# * During a cached failure window, API requests receive error responses until a successful load
# - Requests received while the initial load is in progress will block until it completes, then
# receive either a normal response (on success) or an error response (on failure)

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +25
* The cache is initialized once at startup and never revalidated, ensuring
* the cycle config set JSON is only fetched once during the application lifecycle.
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

This comment says the cycle config set is “never revalidated” and “only fetched once”, but the underlying cache is configured with errorTtl and will retry after failures (and read() will also await the initial load if it hasn’t completed). Please adjust the wording to clarify: it never revalidates after a successful load, but it can retry on error states.

Suggested change
* The cache is initialized once at startup and never revalidated, ensuring
* the cycle config set JSON is only fetched once during the application lifecycle.
* After a successful load the cache is not revalidated, but it may retry on error
* states (for example, based on `errorTtl`), and `read()` will await the initial
* load if it has not yet completed.

Copilot uses AI. Check for mistakes.
type ReferralLeaderboardCyclesCacheMap,
} from "@/cache/referral-leaderboard-cycles.cache";
import { factory } from "@/lib/hono-factory";
import { referralProgramCycleConfigSetMiddleware } from "@/middleware/referral-program-cycle-set.middleware";
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

Unused import referralProgramCycleConfigSetMiddleware.

Suggested change
import { referralProgramCycleConfigSetMiddleware } from "@/middleware/referral-program-cycle-set.middleware";

Copilot uses AI. Check for mistakes.
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.

Introduce Referral Program Cycles

2 participants