# HyperSignals Referral & Point Program Backend Design ## Table of Contents 1. [Overview](#overview) - [System Goals](#system-goals) - [Tech Stack](#tech-stack) 2. [Core Design Decisions](#core-design-decisions) - [Point Calculation Architecture](#point-calculation-architecture) - [API Architecture Style](#api-architecture-style) - [Tier State Management](#tier-state-management) - [Claiming Mechanism](#claiming-mechanism) - [Season System](#season-system) 3. [API Design](#api-design) - [Base URL Structure](#base-url-structure) - [Authentication & Rate Limiting](#authentication) - [Referral Endpoints](#referral-endpoints) - [Points Endpoints](#points-endpoints) - [Claiming Endpoints](#claiming-endpoints) - [Dashboard Composite Endpoints](#dashboard-composite-endpoints) - [Admin Endpoints (Internal)](#admin-endpoints-internal-tbd) - [Internal Dashboard Endpoints](#internal-dashboard-endpoints) 4. [User Journey Breakdown](#user-journey-breakdown) - [Referrer Journey](#referrer-journey) - [Referee Journey](#referee-journey) - [Points Journey](#points-journey) - [Admin Journey](#admin-journey-tbd) - [Internal Dashboard Journey](#internal-dashboard-journey) - [Endpoint Reference Summary](#endpoint-reference-summary) 5. [Technical Architecture](#technical-architecture) - [System Overview](#system-overview) - [Claiming Mechanism](#claiming-mechanism-1) - [Point Calculation Engine](#point-calculation-engine) 6. [Database Schema](#database-schema) - [Existing Tables (No Changes)](#existing-tables-no-changes) - [New Tables](#new-tables) 7. [Security & Operations](#security--operations) - [Security Considerations](#security-considerations) - [Monitoring & Observability](#monitoring--observability) - [Data Integrity & Validation](#data-integrity) 8. [Fuul Migration](#fuul-migration) - [Migration Overview](#migration-overview) - [Data Migration Strategy](#data-migration-strategy) - [Double-Claiming Prevention](#double-claiming-prevention) - [Verification Strategy](#verification-strategy) --- <a id="overview"></a> ## 1. Overview <a id="system-goals"></a> ### 1.1 System Goals - Track referrer-referee relationships via unique referral codes - Calculate and distribute referral revenue based on tiered system - Manage weekly point distribution (organic + referral pools) - Handle claiming mechanism with smart contract synchronization - Support Master Account / Agent Account architecture - Replace the existing Fuul integration with an in-house system, ensuring zero disruption during migration - Potentially support multiple DEXs with a modular architecture that makes onboarding new exchanges straightforward <a id="tech-stack"></a> ### 1.2 Tech Stack - **Language:** Rust - **Databases (Dual-Database Architecture):** - **PostgreSQL:** User identity, referral relationships, points/claims snapshots - **TimescaleDB:** Trading data (764M+ trades) with continuous aggregates for pre-computed stats - **Cross-Database Strategy:** Application-level queries with separate connection pools (FDW is a potential future alternative) **Why Both Databases Are Required:** | Database | What It Provides | |----------|------------------| | **PostgreSQL** | User ↔ wallet mapping, referral relationships, VIP tier status, snapshot storage | | **TimescaleDB** | Trade data (volume, fees, PnL), pre-aggregated via continuous aggregates | Neither database alone is sufficient. TimescaleDB only knows wallet addresses; PostgreSQL provides user identity and relationships. --- <a id="core-design-decisions"></a> ## 2. Core Design Decisions <a id="point-calculation-architecture"></a> ### 2.1 Point Calculation Architecture **Design: Pre-computed Snapshots (Batch Processing)** Run scheduled jobs to compute and store point snapshots. **Key characteristics:** - Fast API responses (read from pre-computed data) - Predictable database load - Easy to compute rankings - Can run snapshots frequently (every 15-30 min) for near real-time feel - Aligns with weekly point program model --- <a id="api-architecture-style"></a> ### 2.2 API Architecture Style **Design: REST with Composite Endpoints** Standard REST endpoints plus aggregate endpoints for common UI needs. ``` Individual endpoints: GET /api/v1/referral/code/{address} GET /api/v1/points/{address} POST /api/v1/referral/apply Composite endpoints: GET /api/v1/dashboard/referrer/{address} → all referrer data in one call GET /api/v1/dashboard/referee/{address} → all referee data in one call ``` **Key characteristics:** - REST simplicity with single-call dashboard loading - Easy to cache at both individual and composite levels - Simple to implement in Rust (axum) - Predictable data needs make composites straightforward --- <a id="tier-state-management"></a> ### 2.3 Tier State Management **Design: Cron-Based Snapshots with Display Caching** Tier is calculated from cumulative referee volume, stored in `tier_history`, and cached for dashboard display. **Tier Thresholds:** | Tier | Volume Threshold | Revenue Share | |------|-----------------|---------------| | Bronze | $0 | 40% | | Silver | $25M | 50% | | Gold | $100M | 60% | | VIP | Admin grant | 70% | **Calculation Flow:** | Use Case | Approach | Freshness | |----------|----------|-----------| | **Dashboard display** | In-memory cache (15-min TTL) | Display only, no payout impact | | **Payout calculation** | Fresh query from TimescaleDB | Always current at cron execution | **Key Insight:** Caching is **display-only**. Actual payouts are calculated by the daily cron job, which queries the current tier at execution time—not from any cache. | Concern | Impact | |---------|--------| | Tier upgrade during cache period | Dashboard shows old tier temporarily; **payout unaffected** | | VIP grant/revoke | Immediate in `tier_history`; cron picks up on next run | Tier can only **increase** (volume is cumulative). A stale cache means the user temporarily sees a lower tier, but their actual payout is unaffected. --- <a id="claiming-mechanism"></a> ### 2.4 Claiming Mechanism **Design: Merkle Tree Based Claiming with Cumulative Amounts** Periodically compute Merkle root of all claimable balances, publish to contract. ``` [Batch Compute] → [Merkle Root] → [Contract Update] [User Claim] → [Contract verifies proof] → [Payout] ``` **Key characteristics:** - Decoupled: contract is source of truth for claim state - No double-claim possible (contract tracks cumulative claimed amounts) - Gas efficient (single root update covers all users) - Users can claim anytime with proof - Industry standard (Uniswap, Aave, etc.) **Cumulative Amount Model:** Merkle leaves store **cumulative** (total ever earned) amounts, not incremental: | Event | User Earns | Cumulative in Leaf | Claimed On-Chain | Claimable | |-------|------------|-------------------|------------------|-----------| | Week 1 | $100 | $100 | $0 | $100 | | User claims | - | $100 | $100 | $0 | | Week 2 | $50 | $150 | $100 | $50 | | Week 3 | $30 | $180 | $100 | $80 | | User claims | - | $180 | $180 | $0 | **How it works:** 1. Merkle leaf = `(address, cumulative_amount)` 2. Contract stores `claimed[address]` = total already claimed 3. On claim: payout = `cumulative_amount - claimed[address]` 4. Contract updates: `claimed[address] = cumulative_amount` **Benefits:** - Same contract reused forever (just update Merkle root) - Users can skip weeks and claim later (no "use it or lose it") - Mixed claim states handled naturally (each user independent) - Partial claims work correctly across root updates --- <a id="season-system"></a> ### 2.5 Season System The points program operates in **seasons** (S1, S2, etc.). Each season has its own configuration for pool sizes, multipliers, and referral percentages, allowing flexibility to adjust incentives without code changes. #### Season Concept ``` Season 1 (S1) Season 2 (S2) Season 3 (S3) ───────────────────────────── ───────────────────────────── ───────────── start: 2025-01-01 start: 2025-04-01 start: 2025-07-01 end: 2025-03-31 end: 2025-06-30 end: NULL (ongoing) volume_pool: 405,000 volume_pool: 500,000 volume_pool: 600,000 copy_multiplier: 3.0x copy_multiplier: 2.5x copy_multiplier: 2.0x ``` #### Configurable Parameters (per season) | Parameter | S1 Value | Description | |-----------|----------|-------------| | `volume_pool_size` | 405,000 | Weekly volume pool (90% of 450K organic) | | `loss_pool_size` | 45,000 | Weekly loss pool (10% of 450K organic) | | `referral_pool_size` | 50,000 | Weekly referral pool cap | | `manual_trading_multiplier` | 1.0x | Volume weight for manual trading | | `copy_trading_multiplier` | 3.0x | Volume weight for copy trading | | `manual_loss_multiplier` | 1.0x | Loss weight for manual trading | | `copy_loss_multiplier` | 1.0x | Loss weight for copy trading | | `standard_referral_pct` | 10% | Standard tier: % of referee organic points | | `vip_referral_pct` | 15% | VIP tier: % of referee organic points | | `elite_referral_pct` | 20% | Elite tier: % of referee organic points | | `vip_boost_pct` | 10% | VIP referee boost (fixed) | | `default_elite_boost_pct` | 15% | Default Elite referee boost (overridable per partner) | #### Key Behaviors - **Automatic transition**: Cron jobs determine current season by date - **Historical accuracy**: Past calculations use the config that was active at that time - **Immutable past seasons**: Cannot modify seasons that have ended (for audit integrity) - **Future seasons editable**: Can adjust upcoming season configs before they start - **Leaderboard options**: Per-season or cumulative across all seasons --- <a id="api-design"></a> ## 3. API Design <a id="base-url-structure"></a> ### 3.1 Base URL Structure ``` /api/v1/referral/* - Referral system endpoints /api/v1/points/* - Points system endpoints /api/v1/claims/* - Claiming endpoints /api/v1/dashboard/* - Composite dashboard endpoints ``` <a id="authentication"></a> ### 3.2 Authentication & Rate Limiting #### Authentication **Public endpoints (no auth required):** - All GET endpoints - read operations are public - Points, leaderboards, referral stats are publicly queryable **Authenticated endpoints (signature required):** - `POST /api/v1/referral/code/generate` - create referral code - `POST /api/v1/referral/apply` - apply referral code Signature is included in request body using EIP-712 typed data: ```json { "master_address": "0x123...", "timestamp": 1234567890, "signature": "0xabc...", // signs: address + payload fields + timestamp // ... other payload fields } ``` #### Rate Limiting All endpoints are rate-limited by IP address to prevent abuse. | Setting | Value | |---------|-------| | Rate limit | 100 requests/minute | | Burst | 20 requests | | Key | Client IP address | **Rate Limit Exceeded Response:** ``` HTTP 429 Too Many Requests Retry-After: 45 { "error_code": "AUTH_003", "message": "Rate limit exceeded" } ``` **Implementation:** - Uses `governor` crate with `tower-governor` middleware - In-memory token bucket algorithm - Configurable per environment (relaxed in development) #### Error Codes All API errors return a consistent format with an `error_code` and human-readable `message`: ```json { "error_code": "REF_001", "message": "This referral code is already in use" } ``` **Error Code Reference:** | Code | Category | Description | |------|----------|-------------| | **Referral Errors** | | | | `REF_001` | Referral | Referral code already in use | | `REF_003` | Referral | No referral code found for address | | `REF_004` | Referral | Address already has a referrer | | `REF_005` | Referral | Cannot use own referral code (self-referral) | | `REF_006` | Referral | Referral code not found or inactive | | **Claim Errors** | | | | `CLM_001` | Claims | No claimable balance of this type | | **Auth Errors** | | | | `AUTH_001` | Auth | Invalid signature | | `AUTH_002` | Auth | Request timestamp expired | | `AUTH_003` | Auth | Rate limit exceeded | | **Validation Errors** | | | | `VAL_001` | Validation | Invalid address format | | `VAL_002` | Validation | Invalid referral code format | | `VAL_003` | Validation | Missing required field | --- <a id="referral-endpoints"></a> ### 3.3 Referral Endpoints #### 1. Generate/Get Referral Code > **Note:** Any user can generate a referral code immediately upon connecting their wallet. No volume requirement exists for code creation. ``` POST /api/v1/referral/code/generate (auth required) Body: { "master_address": "0x969...200", "code": "CRYPTOKING", // required, 3-15 alphanumeric chars "timestamp": 1234567890, "signature": "0xabc..." // EIP-712 signature } Response 201: { "code": "CRYPTOKING", "referral_link": "https://app.hypersignals.ai/trade?ref=CRYPTOKING", "created_at": "2025-01-20T15:00:00Z" } Response 400: { "error_code": "REF_001", "message": "This referral code is already in use" } ``` > **Note:** Referral codes are permanent and cannot be changed once created. Users must provide their own code—no random/default code generation is supported. This eliminates confusion since codes cannot be modified after creation. The UI must require code input before allowing generation. #### 2. Get Referral Code for Address ``` GET /api/v1/referral/code/{address} Response 200: { "code": "CRYPTOKING", "referral_link": "https://app.hypersignals.ai/trade?ref=CRYPTOKING", "is_active": true, "created_at": "2025-01-20T15:00:00Z" } Response 404: { "error_code": "REF_003", "message": "No referral code found for this address" } ``` #### 3. Apply Referral Code (Link Referee to Referrer) ``` POST /api/v1/referral/apply (auth required) Body: { "referee_address": "0x456...789", "referral_code": "CRYPTOKING", "timestamp": 1234567890, "signature": "0xabc..." // EIP-712 signature from referee } Response 201: { "success": true, "referrer_code": "CRYPTOKING", "fee_discount_percentage": 8.0, "message": "You now save 8% on all trading fees!" } Response 400: { "error_code": "REF_004", "message": "This address already has a referrer" } Response 400: { "error_code": "REF_005", "message": "Cannot use your own referral code" } Response 404: { "error_code": "REF_006", "message": "Referral code not found or inactive" } ``` #### 4. Get Referrer Tier ``` GET /api/v1/referral/tier/{address} Response 200: { "address": "0x969...200", "current_tier": "silver", "revenue_share_percentage": 50.0, "bps_rate": 2.5, "lifetime_referred_volume": 35000000.00, "next_tier": "gold", "next_tier_threshold": 100000000.00, "progress_to_next_tier": 35.0, "tier_history": [ { "tier": "bronze", "unlocked_at": "2025-01-20T15:00:00Z", "volume_at_unlock": 0 }, { "tier": "silver", "unlocked_at": "2025-03-15T10:00:00Z", "volume_at_unlock": 25000000.00 } ] } ``` #### 5. Get Referrer's Referees ``` GET /api/v1/referral/referees/{address}?limit=20&offset=0 Response 200: { "referrer_address": "0x969...200", "referees": [ { "address": "0x456...789", "joined_at": "2025-02-01T10:00:00Z", "total_volume": 500000.00, "total_fees_generated": 250.00, "your_earnings": 125.00 } ], "total": 45, "offset": 0, "limit": 20 } ``` --- <a id="points-endpoints"></a> ### 3.4 Points Endpoints #### 1. Get Points Summary ``` GET /api/v1/points/{address} Response 200: { "address": "0x969...200", "current_season": { "season_number": 1, "season_name": "Season 1" }, "current_week": { "week_start": "2025-01-20", "week_end": "2025-01-26", "volume_points": 3000, "loss_points": 150, "boost_points": 630, // If referred via boosted link "referral_pool_points": 330, "total_points": 4110, "rank": 90, "total_participants": 190 }, "season_total": { "total_points": 45000, // Points earned in current season "season_rank": 85 }, "lifetime": { "total_points": 125000, // Points across all seasons "total_volume_points": 100000, "total_loss_points": 5000, "total_boost_points": 10000, "total_referral_points": 10000 }, "boost_status": { "is_boosted": true, "boost_percentage": 18.0, // Actual boost % from referrer's config "referrer_code": "CRYPTOKING", "referrer_tier": "elite" // VIP: 10% fixed, Elite: configurable } } ``` #### 2. Get Points Leaderboard ``` GET /api/v1/points/leaderboard?limit=50&offset=0&season=current Query Parameters: - season: "current" (default), "all" (lifetime), or season number (1, 2, etc.) - limit: max results (default 50) - offset: pagination offset Response 200: { "season": { "season_number": 1, "season_name": "Season 1" }, "leaderboard": [ { "rank": 1, "address": "0xabc...def", "total_points": 150000, "volume_points": 120000, "loss_points": 5000, "boost_points": 25000 } ], "total": 190, "offset": 0, "limit": 50 } ``` --- <a id="claiming-endpoints"></a> ### 3.5 Claiming Endpoints #### 1. Get Claimable Balances ``` GET /api/v1/claims/{address}/balance Response 200: { "address": "0x969...200", "balances": { "referral_revenue": { "claimable": 12450.50, "currency": "USDC", "last_updated": "2025-01-25T12:00:00Z" }, "referee_savings": { "claimable": 245.80, "currency": "USDC", "last_updated": "2025-01-25T12:00:00Z" } }, "total_claimable_usd": 12696.30, "minimum_claim_threshold": 1.00, // UI display only - not enforced by backend "merkle_root_version": "0xabc...def", "next_merkle_update": "2025-01-26T00:00:00Z" } ``` #### 2. Get Merkle Proof for Claiming ``` GET /api/v1/claims/{address}/proof?type=referral_revenue Response 200: { "address": "0x969...200", "claim_type": "referral_revenue", "cumulative_amount": "12450500000", "claimed_amount": "0", "claimable_amount": "12450500000", "merkle_root": "0xabc...def", "proof": [ "0x123...456", "0x789...abc", "0xdef...012" ], "leaf_index": 42, "contract_address": "0xCLAIM_CONTRACT" } Response 400 (nothing to claim): { "error_code": "CLM_001", "message": "No claimable balance of this type", "cumulative_amount": "5000000000", "claimed_amount": "5000000000", "claimable_amount": "0" } ``` #### 3. Get Claim History ``` GET /api/v1/claims/{address}/history?limit=20&offset=0 Response 200: { "address": "0x969...200", "claims": [ { "id": "uuid", "claim_type": "referral_revenue", "amount": 5000.00, "currency": "USDC", "tx_hash": "0xtx...hash", "claimed_at": "2025-01-15T10:00:00Z", "status": "confirmed" } ], "total_claimed": { "referral_revenue": 15000.00, "referee_savings": 500.00 }, "total": 12, "offset": 0, "limit": 20 } ``` --- <a id="dashboard-composite-endpoints"></a> ### 3.6 Dashboard Composite Endpoints #### 1. Referrer Dashboard ``` GET /api/v1/dashboard/referrer/{address} Response 200: { "address": "0x969...200", "referral_code": { "code": "CRYPTOKING", "link": "https://app.hypersignals.ai/trade?ref=CRYPTOKING" }, "tier": { "current": "silver", "revenue_share": 50.0, "next_tier": "gold", "progress": 35.0 }, "stats": { "total_referees": 45, "active_referees": 38, "lifetime_referred_volume": 35000000.00, "total_earnings": 17500.00, "claimable_earnings": 2500.00 }, "ranking": { "rank": 12, "total_referrers": 150 }, "points": { "referral_pool_points_this_week": 330, "lifetime_referral_points": 5000 } } ``` #### 2. Referee Dashboard ``` GET /api/v1/dashboard/referee/{address} Response 200: { "address": "0x456...789", "referral_status": { "is_referred": true, "referrer_code": "CRYPTOKING", "joined_at": "2025-02-01T10:00:00Z", "fee_discount": 8.0 }, "savings": { "total_savings": 500.00, "claimable_savings": 245.80, "currency": "USDC" }, "trading_stats": { "total_volume": 500000.00, "total_fees_paid": 2500.00, "total_fees_saved": 200.00 }, "boost_status": { "is_boosted": true, "boost_percentage": 18.0, // Actual boost from referrer's Elite config "boost_source": "CRYPTOKING", "referrer_tier": "elite" }, "points": { "this_week": 3150, "lifetime": 45000, "rank": 90 } } ``` --- <a id="admin-endpoints-internal-tbd"></a> ### 3.7 Admin Endpoints (Internal) > **Authentication:** All admin endpoints require `x-admin-password` header. Non-view operations are logged to `admin_audit_logs` and trigger Slack notifications. See [7.1 Security Considerations](#security-considerations) for details. #### 1. Grant VIP Tier ``` POST /api/v1/admin/tier/grant-vip Headers: x-admin-password: <admin_password> Body: { "master_address": "0x969...200", "granted_by": "admin@hypersignals.ai", "reason": "Strategic partner agreement" } Response 200: { "success": true, "user_id": "uuid-...", "previous_tier": "gold", "new_tier": "vip", "granted_at": "2025-01-20T12:00:00Z" } ``` #### 2. Configure Partner Tier & Boost ``` POST /api/v1/admin/partner/configure Headers: x-admin-password: <admin_password> Body: { "referral_code": "CRYPTOKING", "referral_points_tier": "elite", // "standard" | "vip" | "elite" "referee_boost_percentage": 18.0, // VIP: fixed 10%, Elite: configurable (e.g., 15-20%) "notes": "Partnership deal Q1 2025" } Response 200: { "success": true, "referral_code": "CRYPTOKING", "referral_points_tier": "elite", "referee_boost_percentage": 18.0, "configured_at": "2025-01-20T12:00:00Z" } ``` #### 3. Season Management **Create Season:** ``` POST /api/v1/admin/seasons Headers: x-admin-password: <admin_password> Body: { "season_number": 2, "season_name": "Season 2", "start_date": "2025-04-01T00:00:00Z", "end_date": "2025-06-30T23:59:59Z", "volume_pool_size": 500000, "loss_pool_size": 50000, "referral_pool_size": 55000, "manual_trading_multiplier": 1.0, "copy_trading_multiplier": 2.5, "standard_referral_pct": 10.0, "vip_referral_pct": 15.0, "elite_referral_pct": 20.0, "vip_boost_pct": 10.0 } Response 201: { "success": true, "season_id": "uuid-...", "season_number": 2, "message": "Season 2 created (starts 2025-04-01)" } Response 400: { "error_code": "SEASON_001", "message": "Season dates overlap with existing Season 1" } ``` **Update Season** (future seasons only): ``` PUT /api/v1/admin/seasons/{season_id} Headers: x-admin-password: <admin_password> Body: { "volume_pool_size": 550000, "copy_trading_multiplier": 2.0 } Response 200: { "success": true, "season_id": "uuid-...", "updated_fields": ["volume_pool_size", "copy_trading_multiplier"] } Response 400: { "error_code": "SEASON_002", "message": "Cannot modify past or active seasons" } ``` **Get Current Season:** ``` GET /api/v1/admin/seasons/current Response 200: { "season_id": "uuid-...", "season_number": 1, "season_name": "Season 1", "start_date": "2025-01-01T00:00:00Z", "end_date": "2025-03-31T23:59:59Z", "volume_pool_size": 405000, "loss_pool_size": 45000, "referral_pool_size": 50000, "manual_trading_multiplier": 1.0, "copy_trading_multiplier": 3.0, "standard_referral_pct": 10.0, "vip_referral_pct": 15.0, "elite_referral_pct": 20.0, "vip_boost_pct": 10.0, "is_active": true } ``` **List All Seasons:** ``` GET /api/v1/admin/seasons Response 200: { "seasons": [ { "season_id": "uuid-1", "season_number": 1, "season_name": "Season 1", "start_date": "2025-01-01T00:00:00Z", "end_date": "2025-03-31T23:59:59Z", "is_active": true, "status": "active" }, { "season_id": "uuid-2", "season_number": 2, "season_name": "Season 2", "start_date": "2025-04-01T00:00:00Z", "end_date": "2025-06-30T23:59:59Z", "is_active": false, "status": "upcoming" } ], "current_season_number": 1 } ``` #### 4. Prepare Merkle Root Update Returns transaction data for the admin wallet to sign. The actual on-chain update is performed by the admin wallet, not the backend. ``` POST /api/v1/admin/merkle/prepare Headers: x-admin-password: <admin_password> Body: { "claim_type": "referral_revenue" } Response 200: { "success": true, "claim_type": "referral_revenue", "merkle_root": "0xnew...root", "total_cumulative_amount": 150000.00, "leaf_count": 500, "tx_data": { "to": "0xCLAIM_CONTRACT", "data": "0x...", // Encoded updateMerkleRoot(claimType, newRoot) "chain_id": 42161 }, "prepared_at": "2025-01-20T12:00:00Z" } ``` **Flow:** 1. Backend computes Merkle tree and stores proofs 2. Returns `tx_data` for admin to sign 3. Admin UI prompts wallet (Ledger, MetaMask, etc.) to sign and submit 4. Contract verifies admin has `PUBLISHER_ROLE` and updates root See [Section 4.4 A2](#a2-publish-merkle-root) for the complete flow. --- <a id="internal-dashboard-endpoints"></a> ### 3.8 Internal Dashboard Endpoints Endpoints for internal dashboard to monitor referral program health and performance. #### 1. List All Referrers ``` GET /api/v1/admin/referrers?limit=50&offset=0&tier=silver&sort=volume_desc Response 200: { "referrers": [ { "user_id": "uuid-...", "master_address": "0x969...200", "referral_code": "CRYPTOKING", "tier": "silver", "revenue_share_percentage": 50.0, "total_referees": 45, "active_referees": 38, "lifetime_referred_volume": 35000000.00, "total_earnings": 17500.00, "pending_earnings": 2500.00, "created_at": "2025-01-20T15:00:00Z" } ], "total": 150, "offset": 0, "limit": 50 } ``` #### 2. Get Referrer Detail with Referee List ``` GET /api/v1/admin/referrers/{address} Response 200: { "referrer": { "user_id": "uuid-...", "master_address": "0x969...200", "referral_code": "CRYPTOKING", "tier": "silver", "tier_history": [...], "is_partner": true, "partner_config": { "referral_points_tier": "elite", "referee_boost_percentage": 18.0, // Configurable per Elite partner "referral_points_percentage": 20.0 // Derived from tier (Elite = 20%) } }, "stats": { "total_referees": 45, "lifetime_referred_volume": 35000000.00, "total_earnings": 17500.00, "total_claimed": 15000.00, "pending_earnings": 2500.00 }, "referees": [ { "user_id": "uuid-...", "master_address": "0x456...789", "joined_at": "2025-02-01T10:00:00Z", "total_volume": 500000.00, "total_fees_generated": 250.00, "referrer_earnings": 125.00, "is_active": true } ] } ``` #### 3. Referral Program Overview (Aggregated Stats) ``` GET /api/v1/admin/referrals/overview Response 200: { "total_referrers": 150, "total_referees": 1200, "referrers_by_tier": { "bronze": 100, "silver": 35, "gold": 12, "vip": 3 }, "total_referred_volume": 850000000.00, "total_revenue_distributed": 425000.00, "total_referee_savings": 68000.00, "period": { "start": "2025-01-01T00:00:00Z", "end": "2025-01-31T23:59:59Z" } } ``` --- <a id="user-journey-breakdown"></a> ## 4. User Journey Breakdown <a id="referrer-journey"></a> ### 4.1 Referrer Journey #### R1. Get or Create Referral Code **Requirement:** User clicks "Refer" tab → Show existing code OR create new code (user must enter 3-15 alphanumeric characters) → Get shareable link **Flow:** 1. Call `GET` to check if code exists (no auth required) 2. If exists → display it 3. If not exists (404) → show create UI requiring code input, then call `POST` (auth required) > **Important:** Users must provide their own code. No random/default code generation is supported since codes cannot be changed after creation. **Step 1: Check existing code (public, no signature)** `GET /api/v1/referral/code/{address}` ``` GET /api/v1/referral/code/0x969...200 ``` **Response (has code):** ```json { "code": "CRYPTOKING", "referral_link": "https://app.hypersignals.ai/trade?ref=CRYPTOKING", "is_active": true, "created_at": "2025-01-20T15:00:00Z" } ``` **Response (no code):** ```json 404 { "error_code": "REF_003", "message": "No referral code found for this address" } ``` **Step 2: Create new code (only if 404, requires signature)** `POST /api/v1/referral/code/generate` ```json { "master_address": "0x969...200", "code": "CRYPTOKING", // required, 3-15 alphanumeric chars "timestamp": 1234567890, "signature": "0xabc..." } ``` **Response:** ```json { "code": "CRYPTOKING", "referral_link": "https://app.hypersignals.ai/trade?ref=CRYPTOKING", "created_at": "2025-01-20T15:00:00Z" } ``` --- #### R2. Referrer Dashboard **Requirement:** Show rank, tier, referred volume, claimable revenue **Endpoint:** `GET /api/v1/dashboard/referrer/{address}` **Request:** ``` GET /api/v1/dashboard/referrer/0x969...200 ``` **Response:** ```json { "address": "0x969...200", "referral_code": { "code": "CRYPTOKING", "link": "https://app.hypersignals.ai/trade?ref=CRYPTOKING" }, "tier": { "current": "silver", "revenue_share": 50.0, "next_tier": "gold", "progress": 35.0 }, "stats": { "total_referees": 45, "lifetime_referred_volume": 35000000.00, "total_earnings": 17500.00, "claimable_earnings": 2500.00 }, "ranking": { "rank": 90, "total_referrers": 190 }, ... } ``` **UI Mapping:** - "Your rank: 90/190" → `ranking.rank` / `ranking.total_referrers` - "Current tier: Silver" → `tier.current` - "Referred volume: $35M" → `stats.lifetime_referred_volume` - "Claimable: $2,500" → `stats.claimable_earnings` --- #### R3. Claim Referral Revenue **Requirement:** Click [Claim Revenue] → Get proof → Submit to contract **Endpoint:** `GET /api/v1/claims/{address}/proof?type=referral_revenue` **Request:** ``` GET /api/v1/claims/0x969...200/proof?type=referral_revenue ``` **Response:** ```json { "address": "0x969...200", "claim_type": "referral_revenue", "cumulative_amount": "5000000000", "claimed_amount": "2500000000", "claimable_amount": "2500000000", "merkle_root": "0xabc...def", "proof": ["0x123...456", "0x789...abc", ...], "contract_address": "0xCLAIM_CONTRACT" } ``` **Field Explanation:** - `cumulative_amount`: Total ever earned (stored in Merkle leaf) - `claimed_amount`: Already claimed on-chain (from contract state) - `claimable_amount`: What user can claim now (`cumulative - claimed`) **UI Flow:** 1. Show `claimable_amount` in modal (not cumulative) 2. User confirms 3. Frontend calls contract with `proof` and `cumulative_amount` 4. Contract calculates payout: `cumulative_amount - claimed[user]` --- <a id="referee-journey"></a> ### 4.2 Referee Journey #### E1. Apply Referral Code **Requirement:** User clicks referral link → Connects wallet → System applies 8% discount → Show confirmation modal **Endpoint:** `POST /api/v1/referral/apply` **Request:** ```json POST /api/v1/referral/apply { "referee_address": "0x456...789", "referral_code": "CRYPTOKING", "timestamp": 1234567890, "signature": "0xabc..." } ``` **Response:** ```json { "success": true, "referrer_code": "CRYPTOKING", "fee_discount_percentage": 8.0, "message": "You now save 8% on all trading fees!" } ``` **UI:** Show confirmation modal with "Saving 8% on builder fees forever!" --- #### E2. Referee Dashboard (Savings Card) **Requirement:** Show fee discount, savings accrued, claimable savings, total volume, ref code applied **Endpoint:** `GET /api/v1/dashboard/referee/{address}` **Request:** ``` GET /api/v1/dashboard/referee/0x456...789 ``` **Response:** ```json { "address": "0x456...789", "referral_status": { "is_referred": true, "referrer_code": "CRYPTOKING", "joined_at": "2025-02-01T10:00:00Z", "fee_discount": 8.0 }, "savings": { "total_savings": 500.00, "claimable_savings": 245.80, "currency": "USDC" }, "trading_stats": { "total_volume": 500000.00, "total_fees_paid": 2500.00, "total_fees_saved": 200.00 }, ... } ``` **UI Mapping:** - "Fee discount: 8%" → `referral_status.fee_discount` - "Claimable savings: $245.80" → `savings.claimable_savings` - "Total volume: $500K" → `trading_stats.total_volume` - "Ref code: CRYPTOKING" → `referral_status.referrer_code` --- #### E3. Claim Referee Savings **Requirement:** Click [Claim Savings] → Get proof → Submit to contract **Endpoint:** `GET /api/v1/claims/{address}/proof?type=referee_savings` **Request:** ``` GET /api/v1/claims/0x456...789/proof?type=referee_savings ``` **Response:** ```json { "address": "0x456...789", "claim_type": "referee_savings", "cumulative_amount": "500000000", "claimed_amount": "254200000", "claimable_amount": "245800000", "merkle_root": "0xdef...123", "proof": ["0xaaa...bbb", ...], "contract_address": "0xCLAIM_CONTRACT" } ``` --- <a id="points-journey"></a> ### 4.3 Points Journey #### P1. Points Dashboard **Requirement:** Show organic points (volume + loss + boost), referral points, total points, rank **Endpoint:** `GET /api/v1/points/{address}` **Request:** ``` GET /api/v1/points/0x969...200 ``` **Response:** ```json { "address": "0x969...200", "current_week": { "week_start": "2025-01-20", "volume_points": 3000, "loss_points": 150, "boost_points": 630, "referral_pool_points": 330, "total_points": 4110, "rank": 90, "total_participants": 190 }, "lifetime": { "total_points": 125000, ... }, "boost_status": { "is_boosted": true, "boost_percentage": 18.0, // Actual boost from referrer's Elite config "referrer_code": "CRYPTOKING", "referrer_tier": "elite" // VIP: 10% fixed, Elite: configurable } } ``` **UI Mapping (from Points Program examples):** - "Organic Points: 3,150" → `volume_points + loss_points` - "Community Boost: 630" → `boost_points` - "Referral Points: 330" → `referral_pool_points` - "Total Points: 4,110" → `total_points` - "Rank: 90/190" → `rank` / `total_participants` --- #### P2. Points Leaderboard (All-Time) **Requirement:** Show all-time leaderboard with cumulative rankings **Endpoint:** `GET /api/v1/points/leaderboard` **Request:** ``` GET /api/v1/points/leaderboard?limit=50&offset=0 ``` **Response:** ```json { "leaderboard": [ { "rank": 1, "address": "0xabc...def", "total_points": 150000, "volume_points": 120000, "loss_points": 5000, "boost_points": 25000 } ], "total": 190, "offset": 0, "limit": 50 } ``` --- <a id="admin-journey-tbd"></a> ### 4.4 Admin Journey > **Note:** Pool sizes, multipliers, and referral percentages are **configurable per season** via `season_configs`. Values shown below are Season 1 defaults. See [2.5 Season System](#season-system) for configuration details. > **Authentication:** All admin endpoints require `x-admin-password` header. Non-view operations are logged and trigger Slack notifications. See [7.1 Security Considerations](#security-considerations). #### A1. Trigger Point Snapshot **Requirement:** Calculate and store point snapshots for current week (can be scheduled or manual) **Endpoint:** `POST /api/v1/admin/snapshots/trigger` **Request:** ``` POST /api/v1/admin/snapshots/trigger Headers: x-admin-password: <admin_password> Body: { "week_start": "2025-01-20" } ``` **Response:** ```json { "success": true, "week_start": "2025-01-20", "users_processed": 190, "total_volume_points": 450000, "total_loss_points": 50000, "total_referral_points": 30000, "computed_at": "2025-01-20T12:00:00Z" } ``` **What it does:** 1. Query `trades` table (TimescaleDB) for week's volume and losses per user (via `wallets` mapping) 2. Apply volume multipliers (copy_trading_multiplier, manual_trading_multiplier) and loss multipliers (copy_loss_multiplier, manual_loss_multiplier) 3. Calculate pro-rata share of volume pool and loss pool using weighted values 4. Calculate referral pool distribution (capped): - Referrer earns percentage of referee's organic points based on tier: - Standard Affiliate: standard_referral_pct - VIP Affiliate: vip_referral_pct - Elite Partner: elite_referral_pct - If total raw referral points > referral_pool_size, apply pro-rata adjustment 5. Apply boost percentages for referred users (minted OUTSIDE the weekly pool): - Standard: No boost - VIP: vip_boost_pct on referee's weekly organic points - Elite: partner_configs.referee_boost_percentage OR default_elite_boost_pct 6. Compute rankings 7. Upsert `point_snapshots` and update `cumulative_points` **Scheduling:** Cron job runs every 15-30 minutes, or trigger manually. --- <a id="a2-publish-merkle-root"></a> #### A2. Publish Merkle Root **Requirement:** Calculate Merkle tree from claimable balances, prepare transaction data, and enable admin wallet to publish to contract. **Endpoint:** `POST /api/v1/admin/merkle/prepare` **Request:** ``` POST /api/v1/admin/merkle/prepare Headers: x-admin-password: <admin_password> Body: { "claim_type": "referral_revenue" } ``` **Response:** ```json { "success": true, "claim_type": "referral_revenue", "merkle_root": "0xabc...def", "total_cumulative_amount": 150000.00, "leaf_count": 500, "tx_data": { "to": "0xCLAIM_CONTRACT", "data": "0x...", "chain_id": 42161 }, "metadata": { "users_with_balance": 500, "largest_balance": 5000.00, "smallest_balance": 0.50 }, "prepared_at": "2025-01-20T12:00:00Z" } ``` **Complete Publishing Flow:** ``` ┌──────────────────────────────────────────────────────────────────────────┐ │ MERKLE PUBLISHING FLOW │ ├──────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. ADMIN TRIGGERS PREPARE │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Backend: POST /api/v1/admin/merkle/prepare │ │ │ │ - Validates admin password │ │ │ │ - Query claimable_balances for all users │ │ │ │ - Build sorted leaves: (address, cumulative_amount) │ │ │ │ - Compute Merkle tree and root hash │ │ │ │ - Store proofs in merkle_proofs table │ │ │ │ - Encode tx_data: updateMerkleRoot(claimType, newRoot) │ │ │ │ - Log to admin_audit_logs + Slack notification │ │ │ │ - Return response with tx_data and metadata │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 2. ADMIN UI DISPLAYS CONFIRMATION │ │ │ Shows: claim_type, leaf_count, total_amount │ │ │ Prompts: "Sign transaction to publish new Merkle root?" │ │ │ │ │ ▼ │ │ 3. ADMIN SIGNS WITH WALLET │ │ │ - Ledger, MetaMask, or other wallet │ │ │ - Signs and submits tx to blockchain │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Claim Contract: updateMerkleRoot(claimType, newRoot) │ │ │ │ - Verifies msg.sender has PUBLISHER_ROLE │ │ │ │ - Updates merkleRoots[claimType] = newRoot │ │ │ │ - Emits MerkleRootUpdated(claimType, newRoot, msg.sender) │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ 4. EVENT LISTENER UPDATES DB STATE │ │ - Listens for MerkleRootUpdated event │ │ - Marks new root as active in merkle_roots table │ │ - Marks previous root as inactive │ │ │ └──────────────────────────────────────────────────────────────────────────┘ ``` **Security Model:** - **Password**: Protects API access (who can prepare) - **Wallet**: Authorizes on-chain state change (who can publish) - **Contract**: Enforces PUBLISHER_ROLE via OpenZeppelin AccessControl **One-Way Data Flow:** - Backend only prepares data; it does NOT need to know if the tx succeeded - Contract is source of truth for active Merkle root - Event listener syncs contract state → database (optional, for display) --- #### A3. Grant VIP Tier **Requirement:** Manually grant VIP tier to strategic partners **Endpoint:** `POST /api/v1/admin/tier/grant-vip` **Request:** ``` POST /api/v1/admin/tier/grant-vip Headers: x-admin-password: <admin_password> Body: { "master_address": "0x969...200", "granted_by": "admin@hypersignals.ai", "reason": "Strategic partner agreement" } ``` **Response:** ```json { "success": true, "master_address": "0x969...200", "previous_tier": "gold", "new_tier": "vip", "revenue_share": 70.0, "granted_at": "2025-01-20T12:00:00Z" } ``` **What it does:** 1. Insert into `tier_history` with `unlocked_by = 'admin_grant'` 2. Log admin action for audit --- #### A4. Configure Partner Tier & Boost **Requirement:** Set up KOL/partner tier and referee boost percentage **Endpoint:** `POST /api/v1/admin/partner/configure` **Request:** ``` POST /api/v1/admin/partner/configure Headers: x-admin-password: <admin_password> Body: { "referral_code": "CRYPTOKING", "referral_points_tier": "elite", // "standard" | "vip" | "elite" "referee_boost_percentage": 18.0, // Required for VIP/Elite (see rules below) "notes": "Partnership deal Q1 2025" // Optional admin notes } ``` **Tier & Boost Rules:** | Tier | Referrer Points % | Referee Boost | Boost Configurability | |------|------------------|---------------|----------------------| | Standard | 10% of referee organic | None (NULL) | N/A | | VIP | 15% of referee organic | 10% | Fixed (enforced by app) | | Elite | 20% of referee organic | 15-20% | Configurable per partner | **Validation Rules:** - `standard`: `referee_boost_percentage` must be NULL (no boost) - `vip`: `referee_boost_percentage` must be 10.0 (application enforces fixed 10%) - `elite`: `referee_boost_percentage` must be provided, typically 15.0-20.0 (configurable) **Response:** ```json { "success": true, "referral_code": "CRYPTOKING", "referral_points_tier": "elite", "referee_boost_percentage": 18.0, "referral_points_percentage": 20.0, // Derived from tier "notes": "Partnership deal Q1 2025", "configured_at": "2025-01-20T12:00:00Z" } ``` **What it does:** 1. Upsert `partner_configs` table 2. Tier determines referrer's cut of referee organic points (Standard 10%, VIP 15%, Elite 20%) 3. Users referred via this code get `referee_boost_percentage` extra organic points (minted OUTSIDE 500K pool) 4. Elite boost is configurable per partner; VIP is fixed at 10% --- #### A5. Update Claimable Balances **Requirement:** Calculate and update claimable amounts from fee revenue **Endpoint:** `POST /api/v1/admin/balances/calculate` **Request:** ``` POST /api/v1/admin/balances/calculate Headers: x-admin-password: <admin_password> Body: { "period_start": "2025-01-13T00:00:00Z", "period_end": "2025-01-20T00:00:00Z" } ``` **Response:** ```json { "success": true, "period_start": "2025-01-13T00:00:00Z", "period_end": "2025-01-20T00:00:00Z", "referrers_updated": 45, "referees_updated": 120, "total_referrer_revenue": 25000.00, "total_referee_savings": 8000.00 } ``` **What it does:** 1. Query `trades` table (TimescaleDB) for fees paid by referred users in period 2. Calculate 8% referee savings per user 3. Calculate referrer earnings based on their tier 4. Update `claimable_balances` (cumulative amounts) 5. Store breakdown in `referee_fee_revenue` --- <a id="internal-dashboard-journey"></a> ### 4.5 Internal Dashboard Journey **D1. Overview Page** - View program-wide stats (total referrers, referees, volume, revenue) - See tier distribution breakdown - **Endpoint:** `GET /api/v1/admin/referrals/overview` **D2. Referrer List Page** - Browse all referrers with pagination - Filter by tier (bronze/silver/gold/vip) - Sort by volume, referees count, or earnings - See at-a-glance: code, tier, referee count, volume, earnings - **Endpoint:** `GET /api/v1/admin/referrers?limit=50&offset=0&tier=silver&sort=volume_desc` **D3. Referrer Detail Page** - Click on a referrer to see full details - View tier history and partner config (tier & boost settings) - See complete referee list with individual stats (volume, fees, earnings) - **Endpoint:** `GET /api/v1/admin/referrers/{address}` | Dashboard View | What It Shows | Endpoint | |----------------|---------------|----------| | Overview | Program stats, tier distribution | `GET /api/v1/admin/referrals/overview` | | Referrer List | All referrers, filterable/sortable | `GET /api/v1/admin/referrers` | | Referrer Detail | Single referrer + all their referees | `GET /api/v1/admin/referrers/{address}` | --- <a id="endpoint-reference-summary"></a> ### 4.6 Endpoint Reference Summary #### UI Features | UI Feature | Endpoint | |------------|----------| | Create referral code | `POST /api/v1/referral/code/generate` | | Get existing referral code | `GET /api/v1/referral/code/{address}` | | Apply referral code | `POST /api/v1/referral/apply` | | Referrer dashboard (all-in-one) | `GET /api/v1/dashboard/referrer/{address}` | | Referee dashboard (all-in-one) | `GET /api/v1/dashboard/referee/{address}` | | Points summary | `GET /api/v1/points/{address}` | | Points leaderboard | `GET /api/v1/points/leaderboard` | | Claim proof (any type) | `GET /api/v1/claims/{address}/proof?type={type}` | | Claimable balances | `GET /api/v1/claims/{address}/balance` | #### Admin Operations | Admin Operation | Endpoint | Trigger | |-----------------|----------|---------| | Point snapshot calculation | `POST /api/v1/admin/snapshots/trigger` | Cron (15-30 min) or manual | | Prepare Merkle root update | `POST /api/v1/admin/merkle/prepare` | Manual (returns tx_data for wallet signing) | | Grant VIP tier | `POST /api/v1/admin/tier/grant-vip` | Manual | | Configure partner tier & boost | `POST /api/v1/admin/partner/configure` | Manual | | Calculate claimable balances | `POST /api/v1/admin/balances/calculate` | Cron (daily) or manual | --- <a id="technical-architecture"></a> ## 5. Technical Architecture <a id="system-overview"></a> ### 5.1 System Overview ``` ┌────────────────────────────────────────────────────────────────────────┐ │ SYSTEM ARCHITECTURE │ ├────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────┐ ┌───────────────────┐ │ │ │ Frontend UI │────▶│ API Gateway │ │ │ │ (React/Next.js) │ │ (Rate Limit, │ │ │ └───────────────────┘ │ Auth, etc.) │ │ │ └─────────┬─────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ RUST BACKEND SERVICE │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Referral │ │ Points │ │ Claims │ │ │ │ │ │ Module │ │ Module │ │ Module │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Admin │ │ Dashboard │ │ Merkle │ │ │ │ │ │ Module │ │ Module │ │ Builder │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ▼ │ │ ┌────────────────────────────┐ ┌────────────────────────────┐ │ │ │ PostgreSQL │ │ TimescaleDB │ │ │ │ - users, wallets │ │ - trades │ │ │ │ - Referrals, Points │ │ - Volumes, PnL │ │ │ │ - Claims │ │ │ │ │ └────────────────────────────┘ └────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ BACKGROUND WORKERS │ │ │ │ │ │ │ │ Cron Jobs: │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Point │ │ Claimable │ Read PostgreSQL + TimescaleDB │ │ │ │ │ Snapshot │ │ Balance │ → Calculate in Rust │ │ │ │ │ (15-30 min) │ │ (Daily) │ → Write to PostgreSQL │ │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ Merkle Publication (Admin-triggered): │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Build │ │ Admin │ │ Publish │ │ │ │ │ │ Merkle │─▶│ Signs │─▶│ to Chain │ │ │ │ │ │ Tree │ │ (Wallet) │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └──────┬──────┘ │ │ │ │ │ │ │ │ │ Event Listener: │ │ │ │ │ ┌─────────────┐ │ │ │ │ │ │ Listen for │◀──── Claimed events ─────┼───────────┐ │ │ │ │ │ Claims │ │ │ │ │ │ │ └─────────────┘ │ │ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ updateMerkleRoot() │ │ ▼ │ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ BLOCKCHAIN │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ Claim Contract │ │ │ │ │ │ - updateMerkleRoot(claimType, root) [PUBLISHER_ROLE] │ │ │ │ │ │ - claim(claimType, amount, proof) → emits Claimed event │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ (Trade data flows via separate data ingestor into TimescaleDB) │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────────────┘ ``` <a id="claiming-mechanism-1"></a> ### 5.2 Claiming Mechanism ``` ┌─────────────────────────────────────────────────────────────────────┐ │ CLAIMING FLOW │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │ │ │ Fee Events │───▶│ Aggregation │───▶│ claimable_balances │ │ │ │ (on-chain) │ │ Service │ │ (PostgreSQL) │ │ │ └─────────────┘ └─────────────┘ └───────────┬─────────────┘ │ │ │ │ │ ┌────────────▼────────────┐ │ │ │ Merkle Tree Builder │ │ │ │ (Scheduled Job) │ │ │ └────────────┬────────────┘ │ │ │ │ │ ┌─────────────┐ ┌────────────▼────────────┐ │ │ │ User UI │◀─────── proofs ─────│ merkle_proofs │ │ │ │ │ │ merkle_roots │ │ │ └──────┬──────┘ └────────────┬────────────┘ │ │ │ │ │ │ │ claim(proof) publish root │ │ │ ▼ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ CLAIM CONTRACT │ │ │ │ - Stores active Merkle root │ │ │ │ - Verifies proofs │ │ │ │ - Tracks claimed amounts per address │ │ │ │ - Distributes USDC │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ ClaimExecuted event │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ EVENT LISTENER │ │ │ │ - Syncs claim_records table │ │ │ │ - Updates claimable_balances (subtract claimed) │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────────┘ ``` #### Contract Interface ```solidity // Simplified claim contract interface interface IClaimContract { // Current Merkle root for each claim type function merkleRoots(uint8 claimType) external view returns (bytes32); // Amount already claimed by address for each type function claimed(address user, uint8 claimType) external view returns (uint256); // Claim function function claim( uint8 claimType, uint256 cumulativeAmount, // Total amount ever earned bytes32[] calldata proof ) external; // Admin: update Merkle root function updateMerkleRoot(uint8 claimType, bytes32 newRoot) external; // Admin: one-time initialization of claimed amounts (for migration) // See Section 8.3 for migration details function initializeClaimed( uint8 claimType, address[] calldata users, uint256[] calldata amounts ) external; // Events event Claimed(address indexed user, uint8 claimType, uint256 amount); event MerkleRootUpdated(uint8 claimType, bytes32 newRoot); event ClaimedInitialized(uint8 claimType, uint256 userCount); } ``` #### Claim Process (User Perspective) 1. **User clicks "Claim"** in UI 2. **Frontend fetches proof** from API: `GET /claims/{address}/proof?type=referral_revenue` 3. **Frontend calls contract**: `claim(claimType, cumulativeAmount, proof)` 4. **Contract verifies**: - Proof validates against current Merkle root - `cumulativeAmount > claimed[user][type]` 5. **Contract pays**: `USDC.transfer(user, cumulativeAmount - claimed[user][type])` 6. **Contract updates**: `claimed[user][type] = cumulativeAmount` 7. **Event emitted**: `Claimed(user, type, amountPaid)` 8. **Backend syncs**: Event listener updates `claim_records` #### Why Cumulative Amounts? Using cumulative (total ever earned) instead of "current claimable" prevents issues: - **Root A**: User has earned $100 total, claims $100 - **Root B published**: User earns $50 more, cumulative = $150 - **User claims**: Contract sees cumulative $150, already claimed $100, pays $50 This approach: - Handles partial claims correctly - Works across multiple Merkle root updates - Prevents double-claiming by design #### One-Way Data Flow Architecture The claiming system uses a **one-way data flow** where the backend is independent of contract state: ``` ┌────────────────────────────────────────────────────────────────────────────┐ │ ONE-WAY DATA FLOW │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ │ BACKEND (PostgreSQL) CONTRACT (On-Chain) │ │ ──────────────────── ─────────────────── │ │ │ │ claimable_balances claimed[address][type] │ │ ├── cumulative_earned ─────X───▶ (not synced!) │ │ └── (backend tracks └── (contract tracks │ │ earnings only) claims only) │ │ │ │ Payout = cumulative_earned (from Merkle leaf) │ │ - claimed[address][type] (read from contract at claim time) │ │ │ └────────────────────────────────────────────────────────────────────────────┘ ``` **Key Design Principles:** 1. **Backend only tracks earnings** (`cumulative_earned`): - Fee revenue accumulated from referee trades - Point calculations and conversions - Never stores "what has been claimed" 2. **Contract is source of truth for claims** (`claimed[address][type]`): - Tracks total amount already claimed per user per type - Enforces no double-claiming via `cumulativeAmount > claimed[user]` check 3. **No sync required**: - Backend does NOT need to know contract's `claimed` state - Merkle proofs are valid regardless of what user has claimed - Contract calculates payout at claim time: `payout = leafAmount - claimed[user]` 4. **Double-claim prevention**: - Contract prevents same amount being claimed twice by updating `claimed[user]` after each claim - Even if user claims multiple times between Merkle updates, they only get what's owed **Benefits:** - **Simpler backend**: No need to listen to claim events for correctness - **No race conditions**: Contract handles all claim state atomically - **Fault tolerant**: Backend failures don't affect claim correctness - **Eventual consistency**: Event listener can optionally sync for UI display, but system works without it <a id="point-calculation-engine"></a> ### 5.3 Point Calculation Engine Pool sizes and multipliers are **configurable per season** via `season_configs`. See [2.5 Season System](#season-system) for details. **Cron Job Flow (every 15-30 minutes):** The point calculation cron job loads the current season config, queries **both** databases, calculates in Rust, and writes snapshots to PostgreSQL: ``` ┌────────────────────────────────────────────────────────────────────┐ │ POINT CALCULATION PIPELINE │ ├────────────────────────────────────────────────────────────────────┤ │ │ │ SCHEDULED TRIGGER (Every 15-30 minutes) │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 1. LOAD CURRENT SEASON CONFIG │ │ │ │ PostgreSQL: SELECT * FROM season_configs │ │ │ │ WHERE start_date <= NOW() │ │ │ │ AND (end_date IS NULL OR end_date > NOW()) │ │ │ │ → pool sizes, multipliers, referral percentages │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 2. READ FROM BOTH DATABASES │ │ │ │ PostgreSQL: users, wallets, referrals │ │ │ │ TimescaleDB: continuous aggregates (volume, losses) │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 3. AGGREGATE BY USER (in Rust) │ │ │ │ - Map wallet addresses → user IDs │ │ │ │ - Sum all wallets per user │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 4. APPLY WEIGHTINGS (from season config) │ │ │ │ Volume: │ │ │ │ - Manual trading: season.manual_trading_multiplier │ │ │ │ - Copy trading: season.copy_trading_multiplier │ │ │ │ Losses: │ │ │ │ - Manual losses: season.manual_loss_multiplier │ │ │ │ - Copy losses: season.copy_loss_multiplier │ │ │ │ - Unclassified wallets: default to manual multipliers │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 5. CALCULATE POOL DISTRIBUTIONS (from season config) │ │ │ │ - Volume Pool: weighted_vol / total × volume_pool_size │ │ │ │ - Loss Pool: weighted_loss / total × loss_pool_size │ │ │ │ - Referral Pool: capped at season.referral_pool_size │ │ │ │ - Standard/VIP/Elite %s from season config │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 6. APPLY BOOSTS (minted OUTSIDE weekly pool) │ │ │ │ - VIP referrer: season.vip_boost_pct to referee │ │ │ │ - Elite referrer: partner_configs.referee_boost_pct │ │ │ │ OR season.default_elite_boost_pct if not configured │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 7. WRITE TO POSTGRESQL │ │ │ │ - Upsert point_snapshots (with season_id reference) │ │ │ │ - Update cumulative_points │ │ │ │ - Compute and store rankings │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────────┘ ``` **Data Sources:** | Data | Source | Aggregate Used | |------|--------|----------------| | Copy trading volume | TimescaleDB | `copy_trading_hourly_stats` | | Manual trading volume | TimescaleDB | `manual_wallets_hourly_stats` | | Realized losses | TimescaleDB | `wallet_hourly_stats.gross_loss` | | User ↔ wallet mapping | PostgreSQL | `wallets` table | | Referral relationships | PostgreSQL | `referrals` table | --- <a id="database-schema"></a> ## 6. Database Schema This system uses a **dual-database architecture** with clear separation of concerns: ``` PostgreSQL TimescaleDB ───────────── ─────────── users trades (hypertable, 764M+ rows) wallets ├── wallet_hourly_stats (CA, real-time) referral_codes ├── copy_trading_hourly_stats (CA) referrals ├── manual_wallets_hourly_stats (CA, real-time) tier_history └── trades_wallet_daily_stats_v2 (CA, real-time) partner_configs point_snapshots copy_trading_wallets (tracking table) claimable_balances manual_wallets (tracking table) merkle_roots claim_records ``` **Cross-Database Query Flow:** ``` Cron Jobs (Rust Application) │ ├──► PostgreSQL: Read user-wallet mapping, referral relationships │ ├──► TimescaleDB: Query continuous aggregates for volume/PnL/fees │ └──► Calculate in application → Write snapshots to PostgreSQL ``` <a id="existing-tables-no-changes"></a> ### 6.1 Existing Tables **PostgreSQL Database:** | Table | Key Columns | Purpose | |-------|-------------|---------| | `users` | user_id, privy_id, email | Core user identity | | `wallets` | user_id, wallet_address, is_primary, type | Master-Agent mapping | **TimescaleDB Database:** | Table | Key Columns | Purpose | |-------|-------------|---------| | `trades` | wallet_address, usd_amount, closed_pnl, fee, builder_fee, event_at | Volume, PnL & fee data (hypertable) | **TimescaleDB Continuous Aggregates (Pre-computed):** | View | Bucket | Real-Time | Use Case | |------|--------|-----------|----------| | `wallet_hourly_stats` | 1 hour | Yes | General wallet stats by exchange | | `copy_trading_hourly_stats` | 1 hour | No | Copy trading wallets only (3x multiplier) | | `manual_wallets_hourly_stats` | 1 hour | Yes | Manual wallets only (1x multiplier) | | `trades_wallet_daily_stats_v2` | 1 day | Yes | Daily wallet aggregations | **Key Data Available from Continuous Aggregates:** - `total_volume` - Trading volume in USD - `gross_profit` / `gross_loss` - PnL breakdown - `total_fee` - Exchange fees - `total_builder_fee` - Builder/referral fees **Master-Agent Mapping** (via `wallets`): - `is_primary=true, type=NULL` → Master Account - `is_primary=false, type='auto'` → Copy Trading Agent - `is_primary=false, type='manual'` → Manual Trading Agent > **Confirmed Assumption:** The `wallets` table is the source of truth for Master-Agent relationships. The main HyperSignals app maintains this table automatically when users create new agents. This system reads from `wallets` when aggregating volumes—no additional sync endpoints are needed. <a id="new-tables"></a> ### 6.2 New Tables **Referral System** | Table | Key Columns | Purpose | |-------|-------------|---------| | `referral_codes` | user_id, code, is_active | Referral codes for qualified users | | `referrals` | referrer_code_id, referee_user_id, applied_at | Referrer-referee relationships | | `tier_history` | user_id, tier, lifetime_volume_at_unlock, effective_from | Tier progression history | | `current_tiers` (view) | user_id, tier | Latest tier per user | **Claiming System (Merkle-based)** | Table | Key Columns | Purpose | |-------|-------------|---------| | `claimable_balances` | user_id, claim_type, amount | Accumulated claimable amounts | | `merkle_roots` | claim_type, merkle_root, total_amount, is_active | Published Merkle roots | | `merkle_proofs` | merkle_root_id, user_id, wallet_address, amount, proof | User proofs for claiming | | `claim_records` | user_id, claim_type, amount, tx_hash | On-chain claim history | **Season System** | Table | Key Columns | Purpose | |-------|-------------|---------| | `season_configs` | season_number, start_date, end_date, volume_pool_size, loss_pool_size, referral_pool_size, manual_trading_multiplier, copy_trading_multiplier, manual_loss_multiplier, copy_loss_multiplier, standard_referral_pct, vip_referral_pct, elite_referral_pct, vip_boost_pct, default_elite_boost_pct, is_active | Per-season configuration for all calculation parameters | **Points System** | Table | Key Columns | Purpose | |-------|-------------|---------| | `point_snapshots` | user_id, season_id, week_start, volume_points, loss_points, boost_points, referral_pool_points | Weekly point calculations (per season) | | `cumulative_points` | user_id, total_points | Lifetime point totals (across all seasons) | | `partner_configs` | referral_code_id, referral_points_tier, referee_boost_percentage | Partner tier & boost configuration | | `retroactive_drops` | user_id, volume_points, loss_points, total_points | One-time retro distribution | **Revenue** | Table | Key Columns | Purpose | |-------|-------------|---------| | `referee_fee_revenue` | referral_id, gross_fees_usd, referrer_share_usd, referrer_tier | Fee revenue per referee | **Admin & Audit** | Table | Key Columns | Purpose | |-------|-------------|---------| | `admin_audit_logs` | action, endpoint, admin_role, request_body, response_body, executed_at, success | Audit trail for admin operations | **Custom Types** - `referral_tier`: ENUM ('bronze', 'silver', 'gold', 'vip') - Revenue share tiers (40/50/60/70%) - `referral_points_tier`: ENUM ('standard', 'vip', 'elite') - Points tiers (10/15/20% of referee organic points) - `claim_type`: ENUM ('referral_revenue', 'referee_savings', 'points') --- <a id="security--operations"></a> ## 7. Security & Operations <a id="security-considerations"></a> ### 7.1 Security Considerations #### Public Endpoint Authentication - Read endpoints (GET) are public - no auth required - Write endpoints (POST) require EIP-712 signature in request body - Signature covers: address + payload fields + timestamp - Timestamp validated to prevent replay attacks (5-minute window) #### Admin Endpoint Authentication Admin endpoints use a tiered approach based on operation sensitivity: | Operation Type | Auth Method | Additional Controls | |----------------|-------------|---------------------| | **View endpoints** (GET) | `x-admin-password` header | None | | **Non-view endpoints** (POST) | `x-admin-password` header | DB audit log + Slack notification | | **Merkle publishing** | `x-admin-password` + Admin wallet signature | On-chain authorization | **Password Management:** - Stored in GCP Secret Manager - Rotated periodically (quarterly recommended) - Different passwords for different environments (dev/staging/prod) **Role-Based Access Control:** Different admin passwords can be configured to grant different access levels: | Role | Access Level | Use Case | |------|--------------|----------| | `viewer` | GET endpoints only | Read-only dashboards, monitoring | | `operator` | GET + POST endpoints | Day-to-day operations, tier grants | | `publisher` | GET + POST + Merkle prepare | Full admin access | Password-to-role mapping is configured in GCP Secret Manager: ``` ADMIN_PASSWORD_VIEWER=<password_1> # View only ADMIN_PASSWORD_OPERATOR=<password_2> # View + POST ADMIN_PASSWORD_PUBLISHER=<password_3> # Full access ``` The application validates the provided password against all configured secrets and determines the caller's role. Requests to endpoints beyond the role's access level return HTTP 403 Forbidden. **Merkle Publishing Authorization:** - Password protects API access (who can prepare) - Wallet signature authorizes on-chain state change (who can publish) - Contract enforces PUBLISHER_ROLE via OpenZeppelin AccessControl - Admin wallets managed via contract's `grantRole` / `revokeRole` #### Rate Limiting - IP-based rate limiting: 100 requests/minute with burst of 20 - Applied globally to all endpoints via `tower-governor` middleware - Returns HTTP 429 with `Retry-After` header when exceeded #### Validation - All address inputs validated with regex - Referral codes sanitized (alphanumeric only) - Amount bounds checking before Merkle inclusion #### Contract Security - Merkle root updates require whitelisted admin wallet (PUBLISHER_ROLE) - Admin roles managed via OpenZeppelin AccessControl - Claim amounts bounded by cumulative totals - Re-entrancy protection on claim function - Pausable in case of emergency --- <a id="monitoring--observability"></a> ### 7.2 Monitoring & Observability #### Key Metrics - Point calculation duration and success rate - Merkle tree generation time - API response latencies (p50, p95, p99) - Claim success/failure rates - Active referrers/referees counts #### Alerts - Point calculation job failures - Merkle root publication failures - Unusual claim patterns (potential exploit) - Database connection issues #### Logging - Structured JSON logging with tracing - Request/response logging (sanitized) - Claim events with full context #### Admin Audit Logging All non-view admin operations are logged to the `admin_audit_logs` table: **Logged Operations:** - `POST /api/v1/admin/tier/grant-vip` - `POST /api/v1/admin/partner/configure` - `POST /api/v1/admin/snapshots/trigger` - `POST /api/v1/admin/balances/calculate` - `POST /api/v1/admin/merkle/prepare` **Audit Log Schema:** ```sql CREATE TABLE admin_audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), action VARCHAR(100) NOT NULL, -- e.g., 'grant_vip', 'configure_partner' endpoint VARCHAR(200) NOT NULL, -- Full endpoint path admin_role VARCHAR(50) NOT NULL, -- 'viewer', 'operator', 'publisher' request_body JSONB, -- Sanitized request payload response_body JSONB, -- Full response payload admin_identifier VARCHAR(100), -- IP or admin ID if available executed_at TIMESTAMPTZ DEFAULT NOW(), success BOOLEAN NOT NULL, error_message TEXT -- If failed ); CREATE INDEX idx_audit_logs_action ON admin_audit_logs(action); CREATE INDEX idx_audit_logs_executed_at ON admin_audit_logs(executed_at); CREATE INDEX idx_audit_logs_admin_role ON admin_audit_logs(admin_role); ``` #### Slack Notifications Admin operations trigger Slack notifications for visibility: **Notification Format:** ``` [ADMIN ACTION] grant_vip Environment: production Role: operator Target: 0x969...200 Reason: Strategic partner agreement Executed at: 2025-01-20 12:00:00 UTC Status: Success ``` **Notification Channel:** `#hs-admin-alerts` (or configured channel) --- <a id="data-integrity"></a> ### 7.3 Data Integrity & Validation The system must maintain strict data integrity across all components. Periodic validation checks ensure the system operates precisely and correctly. #### System Invariants These conditions must **always** hold true. Any violation indicates a critical bug or data corruption. **Claiming Invariants:** | Invariant | Description | Validation Query | |-----------|-------------|------------------| | Total claimed ≤ Total earned | Sum of all on-chain claims must not exceed sum of all cumulative_earned | `SUM(contract.claimed) ≤ SUM(claimable_balances.cumulative_earned)` | | Per-user claimed ≤ earned | Each user's claimed amount ≤ their cumulative earned | `∀ user: claimed[user] ≤ cumulative_earned[user]` | | Merkle root consistency | Active Merkle root in contract matches DB | `contract.merkleRoots[type] == merkle_roots.merkle_root WHERE is_active` | | Proof validity | All stored proofs validate against active root | Verify each `merkle_proofs.proof` against root | **Points Distribution Invariants:** | Invariant | Description | Expected Value | |-----------|-------------|----------------| | Weekly volume pool total | Sum of all volume_points for a week | = `volume_pool_size` (405,000 for S1) | | Weekly loss pool total | Sum of all loss_points for a week | = `loss_pool_size` (45,000 for S1) | | Weekly referral pool total | Sum of all referral_pool_points for a week | ≤ `referral_pool_size` (50,000 cap) | | Organic pool total | volume_points + loss_points | = 450,000 for S1 | | Boost points additive | Boost points minted outside 500K pool | Not deducted from organic pools | **Referral System Invariants:** | Invariant | Description | |-----------|-------------| | Single referrer | Each referee has at most one referrer | | No self-referral | User cannot use their own referral code | | No circular referrals | A→B and B→A cannot both exist | | Tier consistency | Stored tier matches calculated tier from volume | | Revenue share accuracy | `referrer_earnings = referee_fees × tier_percentage` | | Referee savings accuracy | `referee_savings = gross_fees × 0.08` (8%) | #### Reconciliation Dashboard Internal dashboard should display: | Metric | Expected | Actual | Status | |--------|----------|--------|--------| | Total cumulative earned (all types) | - | $X | - | | Total claimed on-chain | - | $Y | - | | Unclaimed balance | - | $X - $Y | ✓ if positive | | Weekly volume pool distributed | 405,000 | 405,000 | ✓ | | Weekly loss pool distributed | 45,000 | 45,000 | ✓ | | Weekly referral pool distributed | ≤50,000 | 48,500 | ✓ | | Users with tier mismatch | 0 | 0 | ✓ | | Invalid Merkle proofs | 0 | 0 | ✓ | --- <a id="fuul-migration"></a> ## 8. Fuul Migration This section outlines the strategy for transitioning from the existing Fuul integration to our in-house referral and points system. <a id="migration-overview"></a> ### 8.1 Migration Overview The migration replaces Fuul's referral tracking and points distribution with our own system while ensuring: - No disruption to active referrers and referees - Accurate tier status preservation - Full preservation of lifetime earnings and claim history - Prevention of double-claiming during transition - Verification of calculation accuracy before cutover **Approach:** Full history migration where Fuul data is imported into our database and contract state is initialized with Fuul claim history. After migration, all data is consistent as if Fuul never existed. <a id="data-migration-strategy"></a> ### 8.2 Data Migration Strategy | Data Type | Source | Approach | |-----------|--------|----------| | **Tier status (Bronze/Silver/Gold)** | Recalculate | Deterministic from `trades` (TimescaleDB)—no Fuul data needed | | **VIP tier status** | Fuul export | Admin-granted, not volume-based—must import | | **Referral relationships** | Fuul export | Who referred whom—not in trading data | | **Cumulative earned amounts** | Recalculate | Deterministic from trades + referral relationships | | **Claimed amounts** | Fuul export | What users already withdrew—for contract initialization | **Why Recalculate Earnings Instead of Importing?** Cumulative earned amounts are fully deterministic from: 1. Raw trading data (volume, fees, losses) in TimescaleDB 2. Referral relationships (imported from Fuul) 3. Season configuration parameters Benefits: - Single source of truth (TimescaleDB trades) - Independent verification—no trust in Fuul's calculations - Catches any historical discrepancies - Simpler Fuul export (just relationships + claimed) **Migration Steps:** 1. **Export from Fuul**: Extract referral relationships, VIP status, and claimed amounts via Fuul API or database export 2. **Map to our schema**: Transform Fuul data to `referral_codes`, `referrals`, and `tier_history` tables 3. **Recalculate earnings**: Compute cumulative earned from trades + imported referral relationships 4. **Handle edge cases**: Deleted codes, inactive referrers, orphaned referees 5. **Validate**: Compare recalculated amounts against Fuul for sanity check (discrepancies indicate calculation differences to investigate) **Key Points:** - Referral relationships are the critical Fuul dependency—everything else derives from trades - VIP tiers are admin-granted and must be imported - Claimed amounts needed for contract initialization only - **Recalculation ensures accuracy**: Any Fuul vs recalculated discrepancies should be investigated before cutover <a id="double-claiming-prevention"></a> ### 8.3 Double-Claiming Prevention **Strategy:** Full history migration with contract initialization—data is fully consistent as if Fuul never existed. **Approach: Recalculate Earnings + Initialize Contract with Claim History** | Component | Action | |-----------|--------| | **Database** | Recalculate cumulative earned from trades → `claimable_balances.cumulative_earned` | | **Contract** | Initialize `claimed[user]` → Fuul claimed amounts | | **First Merkle Tree** | Contains recalculated cumulative earned (total, not just unclaimed) | This approach preserves lifetime earnings data, ensures accuracy via recalculation, and maintains the cumulative model's integrity. **Why This Works:** ``` Example: User with $600 claimed on Fuul After Migration: - claimable_balances.cumulative_earned = $1,000 (recalculated from trades) - contract.claimed[user] = $600 (initialized from Fuul export) - First Merkle leaf = (user, $1,000) When user claims: - payout = $1,000 (leaf) - $600 (contract) = $400 ✓ - User can claim remaining $400 without re-claiming $600 ``` **Contract Initialization Function:** The claim contract requires a one-time admin function for migration: ```solidity /// @notice One-time initialization of claimed amounts from legacy system /// @dev Only callable once by admin during migration function initializeClaimed( uint8 claimType, address[] calldata users, uint256[] calldata amounts ) external onlyAdmin; ``` See [Contract Interface](#contract-interface) for the full interface including this function. **Cutover Process:** 1. Announce maintenance window to users 2. Disable claim functionality in the app 3. Process any pending Fuul claims to completion 4. Export final claimed amounts from Fuul 5. Import referral relationships and VIP status (if not already done) 6. Recalculate cumulative earned from trades → `claimable_balances` 7. Deploy new claim contract 8. Call `initializeClaimed()` with Fuul claimed amounts for each user 9. Publish initial Merkle root with recalculated cumulative earned 10. Re-enable claiming with new system **Benefits of Full Migration:** - Lifetime earnings data preserved in database (recalculated from source) - Dashboards show accurate "total earned" and "total claimed" history - Contract state is fully consistent—same behavior as if we always ran this system - No special-case logic needed for "pre-migration" vs "post-migration" users - Independent verification—earnings derived from trades, not imported from Fuul <a id="verification-strategy"></a> ### 8.4 Verification Strategy **Approach:** Run shadow mode on pre-prod environment before cutover. **Verification Steps:** 1. Deploy new system to pre-prod with production data copy 2. Import referral relationships from Fuul 3. Recalculate cumulative earned from trades 4. Compare recalculated amounts against Fuul values (investigate discrepancies) 5. Verify tier assignments match (volume-based tiers) 6. Verify `initializeClaimed` correctly sets contract state with Fuul claimed amounts 7. Test full claim flow end-to-end (user should receive correct unclaimed balance) 8. Document and resolve any discrepancies **Reconciliation:** - Build scripts to compare recalculated vs Fuul amounts - Investigate any discrepancies (calculation rule differences, manual adjustments, etc.) - Test edge scenarios (inactive users, multiple referral codes, tier boundaries) - Establish sign-off criteria before full cutover **Success Criteria:** - Tier assignments match for >99% of users - Recalculated amounts explainably close to Fuul (discrepancies documented) - All claim proofs validate correctly - Contract `claimed[user]` matches Fuul claimed amounts for all users - Test users can claim correct unclaimed balance (recalculated - claimed)