Skip to content

feat: new affiliate endpoints#183

Merged
kernelwhisperer merged 13 commits intomainfrom
feat/affiliate
Feb 3, 2026
Merged

feat: new affiliate endpoints#183
kernelwhisperer merged 13 commits intomainfrom
feat/affiliate

Conversation

@kernelwhisperer
Copy link
Copy Markdown
Contributor

@kernelwhisperer kernelwhisperer commented Jan 13, 2026

Summary

Implements

Depends on cowprotocol/cms#80

Changes

  • New repo: AffiliatesRepository (get by wallet/code, create, list)
  • New routes:
    • GET /affiliate/:address
    • POST /affiliate/:address (EIP‑712 signed bind, chainId=1, code regex 5–20 A‑Z0‑9_-)
    • GET /ref-codes/:code
    • GET /affiliate/affiliate-stats/:address
    • GET /affiliate/trader-stats/:address
  • Dune integration:
    • uploadCsv API + AffiliateProgramExportServicetable affiliate_program_data
    • Export poller every 5m + export after create
    • Affiliate stats queries w/ cache TTL env

Testing

In .env, set the following

CMS_ENABLED=true
CMS_BASE_URL=http://localhost:1337/api
CMS_API_KEY=....

DUNE_API_KEY=...

Now you can do curl on the new endpoints

curl -s "http://localhost:3001/affiliate/0x6fc1Fb2e17DFf120fa8F838af139aF443070Fd0E"
curl -s "http://localhost:3001/ref-codes/FOOBAR"
curl -s "http://localhost:3001/affiliate/affiliate-stats/0x6fc1Fb2e17DFf120fa8F838af139aF443070Fd0E"
curl -s "http://localhost:3001/affiliate/trader-stats/0x6fc1Fb2e17DFf120fa8F838af139aF443070Fd0E"

Summary by CodeRabbit

Release Notes

  • New Features
    • Added affiliate program registration and management endpoints for creating and retrieving affiliate accounts with reward configurations.
    • Added affiliate and trader statistics endpoints to retrieve performance metrics from analytics platform.
    • Added referral code validation endpoint to check affiliate code availability and retrieve terms.
    • Added automated periodic data export of affiliate program information to analytics platform.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive affiliate program management system, including CMS-backed repository, Dune export services, new API routes for affiliate registration and stats retrieval, periodic data export polling, and comprehensive validation schemas.

Changes

Cohort / File(s) Summary
Affiliate Poller & Config
apps/api/src/app/affiliateProgramExportPoller.ts, apps/api/src/app/config/affiliate.ts, apps/main.ts
Introduces periodic affiliate program data export poller that runs every 5 minutes with in-flight locking; adds affiliate code regex validation; integrates poller lifecycle with server startup/shutdown.
Dependency Injection Setup
apps/api/src/app/inversify.config.ts
Wires affiliate-related repositories and services into DI container with conditional bindings based on CMS and Dune feature flags; configures cache TTL for affiliate stats.
Affiliate Management Routes
apps/api/src/app/routes/affiliate/_address/index.ts, apps/api/src/app/routes/affiliate/_address/affiliate.schemas.ts
Implements GET/POST endpoints for affiliate retrieval and creation with EIP-712 signature verification, conflict detection, and optional Dune export triggering; defines comprehensive JSON schemas for validation.
Affiliate Stats Routes
apps/api/src/app/routes/affiliate/affiliate-stats/_address/index.ts, apps/api/src/app/routes/affiliate/affiliate-stats/_address/affiliateStats.schemas.ts
Adds GET endpoint to fetch affiliate statistics from Dune with caching; includes schema definitions for request/response validation.
Trader Stats Routes
apps/api/src/app/routes/affiliate/trader-stats/_address/index.ts, apps/api/src/app/routes/affiliate/trader-stats/_address/traderStats.schemas.ts
Introduces GET endpoint for trader statistics with Dune integration; defines corresponding validation schemas.
Referral Code Routes
apps/api/src/app/routes/ref-codes/_code/index.ts, apps/api/src/app/routes/ref-codes/_code/refCodes.schemas.ts
Implements GET endpoint to validate affiliate referral codes via CMS with format validation and error handling.
Affiliate Repository (CMS-backed)
libs/repositories/src/repos/AffiliatesRepository/AffiliatesRepository.ts, libs/repositories/src/repos/AffiliatesRepository/AffiliatesRepositoryCms.ts
Defines AffiliatesRepository interface and CMS implementation with methods for fetching/creating affiliates and pagination support across Strapi endpoints.
Affiliate Stats Services
libs/services/src/AffiliateStatsService/AffiliateStatsService.ts, libs/services/src/AffiliateStatsService/AffiliateStatsServiceImpl.ts, libs/services/src/AffiliateStatsService/AffiliateStatsService.*.ts
Implements service for fetching cached trader/affiliate statistics from Dune with type guards, normalizers, constants, and pagination logic.
Affiliate Export Service
libs/services/src/AffiliateProgramExportService/AffiliateProgramExportService.ts, libs/services/src/AffiliateProgramExportService/AffiliateProgramExportServiceImpl.ts
Defines service for exporting affiliate data to Dune with CSV generation, signature-based change detection, and batch upload functionality.
Dune Repository Enhancement
libs/repositories/src/repos/DuneRepository/DuneRepository.ts, libs/repositories/src/repos/DuneRepository/DuneRepositoryImpl.ts
Extends Dune repository with CSV upload capability for table ingestion.
Services & Factories Export
libs/services/src/index.ts, libs/services/src/factories.ts, libs/repositories/src/index.ts, libs/services/src/HooksService/HooksServiceImpl.spec.ts
Re-exports new affiliate services and repositories; adds factory function for repository access; updates test mocks.
Type Utilities
libs/services/src/utils/type-checking-utils.ts
Provides generic runtime type guards (isRecord, isString, isNumeric) and safe numeric conversion with error handling.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant API as API Server
    participant CMS
    participant DI as DI Container
    participant Service as AffiliateStatsService
    participant Dune as Dune Analytics

    Client->>API: GET /affiliate/affiliate-stats/:address
    API->>DI: resolve AffiliateStatsService
    DI-->>API: return service instance
    API->>Service: getAffiliateStats(address)
    
    alt Cache Hit
        Service-->>API: return cached result
    else Cache Miss
        Service->>Dune: executeQuery(query_id, address)
        Dune-->>Service: return rows + metadata
        Service->>Service: normalize & paginate results
        Service->>Service: store in cache (TTL)
        Service-->>API: return result
    end
    
    API-->>Client: 200 {stats}
Loading
sequenceDiagram
    actor Client
    participant API as API Server
    participant CMS
    participant DI as DI Container
    participant Repo as AffiliatesRepository
    participant Sig as Signature Verifier
    participant Export as ExportService
    participant Dune as Dune Analytics

    Client->>API: POST /affiliate/:address (code, walletAddress, signature)
    API->>API: validate address match & code format
    API->>Sig: verify EIP-712 signature
    
    alt Invalid Signature
        API-->>Client: 401 Unauthorized
    else Valid Signature
        API->>DI: resolve AffiliatesRepository
        DI-->>API: return CMS-backed repo
        API->>Repo: check getAffiliateByCode(code)
        Repo->>CMS: fetch affiliate by code
        CMS-->>Repo: null or existing
        
        alt Code Already Exists
            API-->>Client: 409 Conflict
        else Code Available
            API->>Repo: createAffiliate(params)
            Repo->>CMS: POST new affiliate
            CMS-->>Repo: created affiliate
            
            opt isDuneEnabled
                API->>DI: resolve ExportService
                DI-->>API: return export service
                API->>Export: exportAffiliateProgramData()
                Export->>Dune: uploadCsv(affiliate_program_data)
                Dune-->>Export: { success: true }
            end
            
            API-->>Client: 201 {code, createdAt}
        end
    end
Loading
sequenceDiagram
    participant Scheduler as Node Scheduler
    participant Poller as affiliateProgramExportPoller
    participant DI as DI Container
    participant Service as ExportService
    participant Repo as AffiliatesRepository
    participant Dune as Dune Analytics

    Scheduler->>Poller: start()
    Poller->>Poller: immediate export (with lock)
    Poller->>DI: resolve ExportService
    DI-->>Poller: return service
    Poller->>Service: exportAffiliateProgramDataIfChanged(lastSignature)
    Service->>Repo: listAffiliates()
    Repo-->>Service: all affiliates
    Service->>Service: generate signature (rowCount, maxUpdatedAt)
    
    alt Signature Changed
        Service->>Service: build CSV from data
        Service->>Dune: uploadCsv(affiliate_program_data)
        Dune-->>Service: { success: true }
        Service-->>Poller: { uploaded: true, result }
        Poller->>Poller: update lastSignature
    else No Change
        Service-->>Poller: { uploaded: false, result }
    end
    
    Poller->>Poller: log result
    Scheduler->>Poller: repeat in 5 minutes
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • anxolin
  • alfetopito
  • shoom3301

Poem

🐰 Hop along the affiliate trail,
CMS and Dune shall never fail,
With signatures verified and stats cached tight,
Export pollers run every five minutes—what a sight!
Code validated, rewards distributed with care,
A bustling program for traders to share! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: new affiliate endpoints' accurately describes the main change: introducing multiple new HTTP endpoints for affiliate program functionality.

✏️ 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/affiliate

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.

@kernelwhisperer kernelwhisperer force-pushed the feat/affiliate branch 3 times, most recently from 80a306d to a08ad30 Compare January 21, 2026 14:17
@kernelwhisperer kernelwhisperer changed the base branch from main to dune-and-hooks January 21, 2026 14:17
@kernelwhisperer kernelwhisperer marked this pull request as ready for review January 28, 2026 11:45
@kernelwhisperer kernelwhisperer self-assigned this Jan 28, 2026
@kernelwhisperer kernelwhisperer requested a review from a team January 28, 2026 11:45
Base automatically changed from dune-and-hooks to main February 3, 2026 09:24
Copy link
Copy Markdown
Contributor

@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: 5

Caution

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

⚠️ Outside diff range comments (1)
apps/api/src/app/inversify.config.ts (1)

146-163: ⚠️ Potential issue | 🟡 Minor

Use singleton scope to preserve AffiliateStatsService cache.
AffiliateStatsServiceImpl keeps an in-memory cache; toDynamicValue is transient by default and creates a new instance on each resolution. This service is fetched per request (in route handlers), defeating caching. Add .inSingletonScope() to the binding.

♻️ Proposed fix
   apiContainer
     .bind<AffiliateStatsService>(affiliateStatsServiceSymbol)
     .toDynamicValue(
       () => new AffiliateStatsServiceImpl(duneRepository, affiliateStatsCacheTtlMs)
-    );
+    )
+    .inSingletonScope();
🤖 Fix all issues with AI agents
In `@apps/api/src/app/routes/ref-codes/_code/index.ts`:
- Around line 18-28: The module currently resolves affiliatesRepository at
top-level which can throw when the binding is absent; move the
apiContainer.get(affiliatesRepositorySymbol) call into the refCodes Fastify
plugin body after the isCmsEnabled guard so resolution only happens when CMS is
enabled; specifically remove the top-level const affiliatesRepository assignment
and instead call apiContainer.get(affiliatesRepositorySymbol) inside refCodes
(after the early return) and use that local variable for subsequent logic.

In `@libs/repositories/src/repos/AffiliatesRepository/AffiliatesRepositoryCms.ts`:
- Around line 97-110: createAffiliate stores raw code and walletAddress which
differs from the normalized values used by getAffiliateByWalletAddress and
getAffiliateByCode, causing unreachable records and duplicates; before calling
cmsPost in createAffiliate, normalize the inputs (e.g., trim and lowercase the
walletAddress and code using the same normalization logic/functions used by the
lookup methods) and persist those normalized values (keep signedMessage and
enabled handling intact) so stored records match lookup behavior.

In `@libs/repositories/src/repos/DuneRepository/DuneRepositoryImpl.ts`:
- Around line 129-147: The uploadCsv implementation in DuneRepositoryImpl is
passing a pre-stringified body to makeRequest which prevents makeRequest from
setting the JSON Content-Type header; update uploadCsv (function uploadCsv and
the call to this.makeRequest) to pass the payload object (not
JSON.stringify(payload)) so makeRequest can serialize and set headers, or
alternatively include a headers: {'Content-Type':'application/json'} in the
options if you must keep a stringified body; ensure the call to
this.makeRequest<UploadCsvResponse>('/uploads/csv', { method: 'POST', body: ...
}) sends a plain object body or explicit JSON header.

In
`@libs/services/src/AffiliateProgramExportService/AffiliateProgramExportServiceImpl.ts`:
- Around line 116-149: The CSV builder in buildCsv and CSV_HEADERS omits the
AffiliateProgramRow.updated_at column, causing exported CSVs to drop that field;
update CSV_HEADERS to include 'updated_at' and add row.updated_at into the array
constructed inside buildCsv (ensuring it goes through csvEscape like the other
fields) so the header and row values stay aligned and the exported CSV contains
the updated_at values for each AffiliateProgramRow.

In `@libs/services/src/HooksService/HooksServiceImpl.spec.ts`:
- Around line 61-63: The MockDuneRepository.uploadCsv mock currently has no
parameters and thus does not match the DuneRepository.uploadCsv signature;
update the MockDuneRepository.uploadCsv method to accept the same parameters and
return type as the DuneRepository interface (match parameter names/types and the
Promise<{ success: boolean; message?: string }> return), so the class correctly
implements DuneRepository and compiles/tests pass.
🧹 Nitpick comments (1)
apps/api/src/main.ts (1)

4-17: Add poller teardown on server shutdown.

Line 17: the poller likely installs an interval; without cleanup, shutdown/tests can leak timers. Consider returning a disposer from startAffiliateProgramExportPoller and wiring it to server.addHook('onClose', ...).

♻️ Proposed change in this file
-startAffiliateProgramExportPoller();
+const stopAffiliateProgramExportPoller = startAffiliateProgramExportPoller();
+server.addHook('onClose', async () => {
+  await stopAffiliateProgramExportPoller?.();
+});

Example update to the poller signature (separate file):

export function startAffiliateProgramExportPoller(): () => void {
  const intervalId = setInterval(run, POLL_INTERVAL_MS);
  run();
  return () => clearInterval(intervalId);
}

@kernelwhisperer kernelwhisperer merged commit 8338e47 into main Feb 3, 2026
9 checks passed
@kernelwhisperer kernelwhisperer deleted the feat/affiliate branch February 3, 2026 11:16
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.

3 participants