Skip to content

feat: referral code#555

Closed
antoncoding wants to merge 14 commits into
masterfrom
codex/referral-tracking
Closed

feat: referral code#555
antoncoding wants to merge 14 commits into
masterfrom
codex/referral-tracking

Conversation

@antoncoding
Copy link
Copy Markdown
Owner

@antoncoding antoncoding commented May 30, 2026

Summary by CodeRabbit

  • New Features

    • Referral codes auto-captured from URLs and attributed when referred users transact.
    • New referral rewards UI to create/share referral links with wallet signing and copy-to-clipboard.
    • Platform-fee tracking added across leverage/rebalance flows; transaction toasts now include fee events.
    • Server endpoints added to accept referral attributions, referral code creation, and platform-fee reports.
  • Documentation

    • Expanded guidance for server-only API tokens and using a private internal origin for server-to-server writes.
  • Chore

    • Example environment file updated with new server-only configuration entries.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 30, 2026

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

Project Deployment Actions Updated (UTC)
monarch Ready Ready Preview, Comment May 31, 2026 3:33pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Captures referral codes from URLs, provides a UI to create/share referral links, verifies signed wallet requests, attributes referrals and records platform-fee events on confirmed transactions, and forwards validated writes to the internal data-api using server-only origin and admin key.

Changes

Referral and Platform Fee Tracking

Layer / File(s) Summary
Server infrastructure and environment
.env.local.example, docs/TECHNICAL_OVERVIEW.md, docs/VALIDATIONS.md, src/utils/dataApiInternal.ts, src/utils/requestNonce.ts, src/utils/signedWalletRequest.ts, app/api/api-keys/route.ts
Adds DATA_API_INTERNAL_ORIGIN and DATA_API_INTERNAL_ADMIN_KEY docs; callDataApiInternal validates admin key and origin and POSTs JSON with X-Internal-Admin-Key and cache: 'no-store'. Adds server-side signed-wallet verification, a request-nonce helper, and refactors api-keys route to use the verifier.
Referral utilities, code generation, and attribution APIs
src/utils/referralRequest.ts, src/utils/referrals.ts, app/api/referrals/code/route.ts, app/api/referrals/attribute/route.ts
Adds referral message build/parse helpers, localStorage normalization/storage utilities, signed /api/referrals/code to create referral codes, and /api/referrals/attribute to submit validated attributions.
Platform fee tracking hook and API
src/hooks/usePlatformFeeTracking.ts, app/api/platform-fees/route.ts
Exports PlatformFeeEventInput and usePlatformFeeTracking to filter/submit events concurrently to /api/platform-fees; server route validates payload and forwards to internal data-api.
URL referral capture and rewards UI
src/components/providers/ReferralTrackingProvider.tsx, app/layout.tsx, src/features/rewards/referral-rewards-block.tsx, src/features/rewards/rewards-view.tsx
Provider captures ref/referral query params and stores them once; ReferralRewardsBlock creates/signed referral links with clipboard fallback and is integrated into the rewards page and root layout provider tree.
Transaction integration
src/hooks/useLeverageTransaction.ts, src/hooks/useSmartRebalance.ts, src/hooks/useRebalanceExecution.ts, src/hooks/useTransactionWithToast.tsx, src/hooks/useReferralAttributionTracking.ts
Leverage and smart-rebalance compute platformFeeEvents and pass them through useRebalanceExecutionuseTransactionWithToast; useTransactionWithToast invokes referral attribution and platform-fee tracking on confirmed transactions.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UI as ReferralRewardsBlock
  participant CodeAPI as /api/referrals/code
  participant DataAPI as data-api /internal

  User->>UI: Click "Create referral link"
  UI->>CodeAPI: POST { address, signature, message }
  CodeAPI->>DataAPI: POST /internal/referrals/code
  DataAPI-->>CodeAPI: { code }
  CodeAPI-->>UI: { code, referrerWallet }
  UI->>User: Show/share referral URL
Loading
sequenceDiagram
  participant TxHook as useTransactionWithToast
  participant FeeHook as usePlatformFeeTracking
  participant AttrHook as useReferralAttributionTracking
  participant FeeAPI as /api/platform-fees
  participant AttrAPI as /api/referrals/attribute
  participant DataAPI as data-api /internal

  TxHook->>TxHook: Transaction confirmed (hash + chainId)
  TxHook->>AttrHook: trackReferralAttribution(chainId, txHash)
  AttrHook->>AttrAPI: POST {referredWallet, referralCode, chainId, txHash}
  AttrAPI->>DataAPI: POST /internal/referrals/attribute

  par Fee submission
    TxHook->>FeeHook: trackPlatformFeeEvents(chainId, txHash, events[])
    FeeHook->>FeeAPI: POST each event
    FeeAPI->>DataAPI: POST /internal/platform-fees
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: referral code' is directly related to the main change—a comprehensive referral system implementation with code generation, tracking, and attribution.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/referral-tracking

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.

@coderabbitai coderabbitai Bot added feature request Specific feature ready to be implemented ui User interface labels May 30, 2026
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces platform fee tracking and referral attribution features, including new API routes, a client-side referral tracking provider, and UI components for managing referral links. The feedback focuses on ensuring data consistency by normalizing wallet addresses, token addresses, and transaction hashes to lowercase, as well as using shared utilities for referral code validation. Additionally, the reviewer recommends addressing potential information disclosure vulnerabilities by logging internal server errors on the server rather than exposing them directly to the client.

Comment on lines +20 to +41
const userWallet = readString(body.userWallet);
const chainId = typeof body.chainId === 'number' ? body.chainId : Number.NaN;
const txHash = readString(body.txHash);
const source = readString(body.source);
const tokenAddress = readString(body.tokenAddress);
const amountRaw = readString(body.amountRaw);

if (
!userWallet ||
!isAddress(userWallet) ||
!Number.isInteger(chainId) ||
!txHash ||
!TX_HASH_PATTERN.test(txHash) ||
!source ||
!tokenAddress ||
!isAddress(tokenAddress) ||
!amountRaw ||
!/^[0-9]+$/.test(amountRaw) ||
BigInt(amountRaw) <= 0n
) {
return NextResponse.json({ error: 'Invalid platform fee request.' }, { status: 400 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Normalize the wallet, token address, and transaction hash to lowercase to prevent case-sensitivity mismatches in the database. Additionally, validate the length of the source parameter to prevent excessively long strings from being processed.

  const userWallet = readString(body.userWallet)?.toLowerCase();
  const chainId = typeof body.chainId === 'number' ? body.chainId : Number.NaN;
  const txHash = readString(body.txHash)?.toLowerCase();
  const source = readString(body.source);
  const tokenAddress = readString(body.tokenAddress)?.toLowerCase();
  const amountRaw = readString(body.amountRaw);

  if (
    !userWallet ||
    !isAddress(userWallet) ||
    !Number.isInteger(chainId) ||
    !txHash ||
    !TX_HASH_PATTERN.test(txHash) ||
    !source ||
    source.length > 64 ||
    !tokenAddress ||
    !isAddress(tokenAddress) ||
    !amountRaw ||
    !/^[0-9]+$/.test(amountRaw) ||
    BigInt(amountRaw) <= 0n
  ) {
    return NextResponse.json({ error: 'Invalid platform fee request.' }, { status: 400 });
  }

Comment thread app/api/platform-fees/route.ts Outdated
Comment on lines +62 to +64
} catch (error) {
return NextResponse.json({ error: error instanceof Error ? error.message : 'Failed to record platform fee.' }, { status: 500 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

Avoid exposing internal server error messages directly to the client to prevent potential information disclosure. Log the error on the server and return a generic error message instead.

  } catch (error) {
    console.error('Error recording platform fee:', error);
    return NextResponse.json({ error: 'Failed to record platform fee.' }, { status: 500 });
  }

Comment on lines +1 to +3
import { type NextRequest, NextResponse } from 'next/server';
import { isAddress } from 'viem';
import { callDataApiInternal } from '@/utils/dataApiInternal';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Import normalizeReferralCode to validate and normalize the referral code consistently on both client and server.

Suggested change
import { type NextRequest, NextResponse } from 'next/server';
import { isAddress } from 'viem';
import { callDataApiInternal } from '@/utils/dataApiInternal';
import { type NextRequest, NextResponse } from 'next/server';
import { isAddress } from 'viem';
import { callDataApiInternal } from '@/utils/dataApiInternal';
import { normalizeReferralCode } from '@/utils/referrals';

Comment on lines +20 to +34
const referredWallet = readString(body.referredWallet);
const referralCode = readString(body.referralCode);
const chainId = typeof body.chainId === 'number' ? body.chainId : Number.NaN;
const txHash = readString(body.txHash);

if (
!referredWallet ||
!isAddress(referredWallet) ||
!referralCode ||
!Number.isInteger(chainId) ||
!txHash ||
!TX_HASH_PATTERN.test(txHash)
) {
return NextResponse.json({ error: 'Invalid referral attribution request.' }, { status: 400 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Normalize the referred wallet and transaction hash to lowercase. Use the shared normalizeReferralCode utility to validate and normalize the referral code format and length.

Suggested change
const referredWallet = readString(body.referredWallet);
const referralCode = readString(body.referralCode);
const chainId = typeof body.chainId === 'number' ? body.chainId : Number.NaN;
const txHash = readString(body.txHash);
if (
!referredWallet ||
!isAddress(referredWallet) ||
!referralCode ||
!Number.isInteger(chainId) ||
!txHash ||
!TX_HASH_PATTERN.test(txHash)
) {
return NextResponse.json({ error: 'Invalid referral attribution request.' }, { status: 400 });
}
const referredWallet = readString(body.referredWallet)?.toLowerCase();
const referralCode = normalizeReferralCode(readString(body.referralCode));
const chainId = typeof body.chainId === 'number' ? body.chainId : Number.NaN;
const txHash = readString(body.txHash)?.toLowerCase();
if (
!referredWallet ||
!isAddress(referredWallet) ||
!referralCode ||
!Number.isInteger(chainId) ||
!txHash ||
!TX_HASH_PATTERN.test(txHash)
) {
return NextResponse.json({ error: 'Invalid referral attribution request.' }, { status: 400 });
}

Comment thread app/api/referrals/attribute/route.ts Outdated
Comment thread app/api/referrals/code/route.ts Outdated
Comment thread app/api/referrals/code/route.ts Outdated
Copy link
Copy Markdown

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/api/referrals/code/route.ts`:
- Around line 30-35: The error branch uses response.status || 502 which can
return a 2xx when upstream responded OK but data.code is missing; change the
status computation before calling NextResponse.json so that if response.ok is
true you force a 502 (or other error status) otherwise use response.status (or
502 fallback). Update the block around NextResponse.json to compute status =
response.ok ? 502 : (response.status ?? 502) and pass that status to
NextResponse.json; refer to response, data.code and NextResponse.json in your
changes.

In `@src/utils/dataApiInternal.ts`:
- Around line 16-24: The fetch call that returns fetch(`${origin}${path}`, { ...
}) has no timeout and can hang; wrap it with an AbortController, pass
controller.signal into the fetch options, and start a timer (e.g., 5–30s) that
calls controller.abort() on timeout; ensure you clear the timer after fetch
resolves or rejects so you don't leak timers; keep the existing headers
(including INTERNAL_ADMIN_HEADER and adminKey), body (JSON.stringify(body)), and
cache option while adding the signal and timer cleanup.

In `@src/utils/referrals.ts`:
- Around line 17-24: storeReferralCodeOnce currently calls
window.localStorage.setItem which can throw (private mode/quota) and bubble into
the caller (ReferralTrackingProvider); wrap the write in a try/catch inside
storeReferralCodeOnce so failures don't throw: after the existing guards
(normalizeReferralCode and getStoredReferralCode) perform the setItem call
inside a try block, catch any error, optionally log it, and return false on
error; reference REFERRAL_CODE_STORAGE_KEY, normalizeReferralCode,
getStoredReferralCode, and storeReferralCodeOnce when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 67a4f661-d675-45fd-b949-702f8ecd6bfe

📥 Commits

Reviewing files that changed from the base of the PR and between c796fe5 and 5311b22.

📒 Files selected for processing (17)
  • .env.local.example
  • app/api/platform-fees/route.ts
  • app/api/referrals/attribute/route.ts
  • app/api/referrals/code/route.ts
  • app/layout.tsx
  • docs/TECHNICAL_OVERVIEW.md
  • src/components/providers/ReferralTrackingProvider.tsx
  • src/features/rewards/referral-rewards-section.tsx
  • src/features/rewards/rewards-landing-view.tsx
  • src/hooks/useLeverageTransaction.ts
  • src/hooks/usePlatformFeeTracking.ts
  • src/hooks/useRebalanceExecution.ts
  • src/hooks/useReferralAttributionTracking.ts
  • src/hooks/useSmartRebalance.ts
  • src/hooks/useTransactionWithToast.tsx
  • src/utils/dataApiInternal.ts
  • src/utils/referrals.ts

Comment thread app/api/referrals/code/route.ts
Comment thread src/utils/dataApiInternal.ts Outdated
Comment thread src/utils/referrals.ts
Copy link
Copy Markdown

@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.

♻️ Duplicate comments (1)
src/utils/dataApiInternal.ts (1)

16-24: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

No timeout on the internal fetch.

If the data API hangs, this call blocks the route handler indefinitely. Add an abort timeout.

Proposed fix
   return fetch(`${origin}${path}`, {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json',
       [INTERNAL_ADMIN_HEADER]: adminKey,
     },
     body: JSON.stringify(body),
     cache: 'no-store',
+    signal: AbortSignal.timeout(10_000),
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/dataApiInternal.ts` around lines 16 - 24, The internal POST fetch
in dataApiInternal.ts has no timeout; wrap the request in an AbortController,
pass controller.signal into fetch, and set a timer (e.g., configurable default
like 5–10s) that calls controller.abort() on timeout; ensure you clear the timer
after fetch completes and handle the abort error path so the route doesn't hang
— update the function that constructs the fetch (the call that uses
INTERNAL_ADMIN_HEADER and adminKey) to use the AbortController and timer and
surface a clear timeout/abort error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/utils/dataApiInternal.ts`:
- Around line 16-24: The internal POST fetch in dataApiInternal.ts has no
timeout; wrap the request in an AbortController, pass controller.signal into
fetch, and set a timer (e.g., configurable default like 5–10s) that calls
controller.abort() on timeout; ensure you clear the timer after fetch completes
and handle the abort error path so the route doesn't hang — update the function
that constructs the fetch (the call that uses INTERNAL_ADMIN_HEADER and
adminKey) to use the AbortController and timer and surface a clear timeout/abort
error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b5b72730-8372-45e8-a26c-3dd318ba4eaf

📥 Commits

Reviewing files that changed from the base of the PR and between c430ed1 and 3dde302.

📒 Files selected for processing (4)
  • .env.local.example
  • docs/TECHNICAL_OVERVIEW.md
  • docs/VALIDATIONS.md
  • src/utils/dataApiInternal.ts
✅ Files skipped from review due to trivial changes (3)
  • docs/VALIDATIONS.md
  • .env.local.example
  • docs/TECHNICAL_OVERVIEW.md

@antoncoding antoncoding force-pushed the codex/referral-tracking branch from 3dde302 to 839f93f Compare May 30, 2026 16:37
Copy link
Copy Markdown

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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/requestNonce.ts`:
- Around line 2-3: The fallback branch in src/utils/requestNonce.ts (the block
where typeof crypto === 'undefined') can produce nonces shorter than the
16-character minimum enforced in src/utils/signedWalletRequest.ts; modify that
branch so the generated string is guaranteed to be at least 16 characters (for
example, build the nonce from Date.now().toString(36) plus one or more
Math.random().toString(36).slice(2) segments and, if needed, loop/apply padding
until the combined length >= 16), so any nonce produced when crypto is
unavailable meets the server's minimum length requirement.

In `@src/utils/signedWalletRequest.ts`:
- Around line 102-104: The nonce is only regex-validated
(NONCE_PATTERN.test(parsedMessage.nonce)) but never recorded so the same signed
payload can be replayed; update signedWalletRequest.ts to atomically
check-and-consume parsedMessage.nonce before returning success (e.g., consult a
nonce store such as Redis or an in-memory cache with the same TTL as the
signature window), reject if the nonce already exists, and insert the nonce when
it is first seen so subsequent calls (including those that reach
createGatewayApiKey in app/api/api-keys/route.ts) are refused; ensure the
check+insert is done atomically or via a SETNX-style operation and uses the same
TTL as the signature expiry.
- Around line 89-96: The origin check using parsedMessage.origin vs
getApplicationOrigin(request) is insufficient because a third party can have the
user sign any origin; instead require a server-issued, single-use challenge
bound to Monarch and verify it was included in the signed payload. Update the
signer flow and the verification in signedWalletRequest.ts so that parsedMessage
must include a server nonce/challenge (e.g., serverChallenge) and you confirm
that serverChallenge matches a stored/issued value tied to the current session
or request context before accepting the signature; keep the origin comparison
but make it secondary to validating the server-issued nonce. Ensure you
reference parsedMessage.origin and getApplicationOrigin(request) in the new
logic and reject if no matching serverChallenge or session is found.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7577723b-b88d-4010-9903-ba6c7a13de8d

📥 Commits

Reviewing files that changed from the base of the PR and between 69c663d and 7564273.

📒 Files selected for processing (9)
  • app/api/api-keys/route.ts
  • app/api/referrals/code/route.ts
  • docs/TECHNICAL_OVERVIEW.md
  • docs/VALIDATIONS.md
  • src/features/api-keys/api-key-console-view.tsx
  • src/features/rewards/referral-rewards-block.tsx
  • src/utils/referralRequest.ts
  • src/utils/requestNonce.ts
  • src/utils/signedWalletRequest.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/VALIDATIONS.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/TECHNICAL_OVERVIEW.md

Comment thread src/utils/requestNonce.ts Outdated
Comment thread src/utils/apiKeySignatureRequest.ts Outdated
Comment thread src/utils/apiKeySignatureRequest.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature request Specific feature ready to be implemented ui User interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant