diggr. ← Back to docs index

DIGGR — Gamification & Virality Playbook

Outil + bot Telegram qui identifie des wallets via cross-référencement de contracts. Système de points pré-airdrop $DIGGR. Cible : early adopters crypto-natifs Solana. Ce doc = idées concrètes, schéma DB, roadmap MVP solo dev.


Sommaire

  1. Top 10 features prioritaires (MVP)
  2. Top 20 features secondaires
  3. Idées audacieuses / différenciantes
  4. Schéma DB consolidé (Postgres)
  5. Roadmap suggérée
  6. Anti-farming layer
  7. Notes de calibration des points

1. Top 10 features prioritaires (MVP)

Critères : ROI engagement/effort maximal, compatibles avec ton schéma actuel, à shipper en 1-2 semaines chacune.


1.1 — Daily Streak

Description. Chaque jour où l'utilisateur complète au moins une daily task, son streak augmente. Casser une journée = retour à 0. À 7/30/100 jours consécutifs : milestone bonus (+50 / +200 / +1000 pts) + badge.

Impact. Fort. C'est LE mécanisme qui ramène les gens chaque jour. Hyperliquid, Backpack, Phantom Quest, Drift l'utilisent tous. Effet de "perte aversion" psychologique très puissant.

Complexité. Simple. Compteur + cron de check journalier UTC.

DB.

alter table users add column current_streak int not null default 0;
alter table users add column longest_streak int not null default 0;
alter table users add column last_streak_day date; -- YYYY-MM-DD UTC

UX clé. Affiche un compteur "12 day streak" en gros sur le dashboard. Warning rouge "Don't break your streak!" quand il reste <4h dans la journée et que la daily n'est pas faite.


1.2 — Daily Combo Multiplier

Description. Si tu complètes TOUTES les daily tasks d'une journée, tu déclenches un X1.5 sur les points gagnés ce jour-là (recalculé à minuit). Si tu fais 7 jours combo d'affilée → X2 permanent jusqu'à ce que tu casses.

Impact. Fort. Force à compléter l'intégralité, pas juste cherry-pick.

Complexité. Moyen. Trigger qui recalcule à la complétion de la dernière task du jour, ou cron à 23:59 UTC.

DB.

alter table users add column daily_multiplier numeric(3,2) not null default 1.0;
alter table users add column combo_streak int not null default 0; -- jours consécutifs avec full combo

UX clé. Barre de progression "3/4 daily tasks done — 1 left to unlock X1.5 today". Animation explosive quand le multiplier s'active.


1.3 — Tier System (Digger Ranks)

Description. 6 ranks basés sur les points cumulés : Pleb -> Scout -> Tracker -> Hunter -> Whale-Watcher -> Diamond Diggr. Chaque rank débloque un avantage concret (ex : Tracker = +1 daily task slot, Hunter = accès au beta du tool, Diamond = NFT soulbound + early access airdrop).

Impact. Fort. Donne un sens de progression long-terme et tease l'utility token sans rien promettre.

Complexité. Simple. Vue calculée à partir de users.points + table tiers config.

DB.

create table tiers (
  id smallint primary key,
  name text not null,
  min_points int not null,
  perks jsonb not null default '{}'
);
-- Pas besoin de stocker le tier sur users : c'est dérivé.
-- Ou pour optim : alter table users add column tier_id smallint references tiers(id);

UX clé. Badge animé sur le profil. Page "Ranks" qui montre tous les tiers + ton avancement vers le suivant ("4,200 / 5,000 pts to Hunter").


1.4 — Multi-Tier Referrals (3 niveaux)

Description. L1 (filleul direct) = +50 pts (existant). L2 (filleul de filleul) = +10 pts au grand-parrain. L3 = +2 pts. Le L2/L3 ne se déclenche que si le L1 atteint un seuil (ex : 500 pts) pour empêcher le spam de wallets vides.

Impact. Fort. C'est ce qui crée des "armies" virales (Hyperliquid, Blast, EigenLayer, Linea).

Complexité. Moyen. Étendre referrals + trigger récursif qui remonte sur 3 niveaux max.

DB.

alter table referrals add column tier_level smallint not null default 1; -- 1, 2 ou 3
alter table referrals add column root_referrer_id uuid references users(id); -- pour query rapide
create index idx_referrals_root on referrals(root_referrer_id);
-- Ou : computed via une vue récursive avec WITH RECURSIVE.

UX clé. Page "Your Squad" avec un arbre dépliable : tes filleuls + leurs filleuls + leur contribution en pts pour toi.


1.5 — Leaderboard Hebdomadaire (avec reset)

Description. En plus du leaderboard all-time top 100 (existant), ajouter un leaderboard hebdomadaire. Top 10 chaque semaine = bonus de 100/50/25 pts + badge "Week 17 Champion". Reset chaque lundi 00:00 UTC.

Impact. Fort. Crée une compétition récurrente, donne aux nouveaux une chance de briller.

Complexité. Moyen. Une vue matérialisée + cron de reset hebdo.

DB.

create table weekly_leaderboard_history (
  week_start date not null,
  user_id uuid references users(id),
  rank int not null,
  weekly_points int not null,
  bonus_awarded int not null default 0,
  primary key (week_start, user_id)
);

-- Pour le live leaderboard : on agrège points_log de la semaine courante.
create materialized view mv_weekly_leaderboard as
select user_id, sum(delta) as weekly_points
from points_log
where created_at >= date_trunc('week', now())
group by user_id
order by weekly_points desc
limit 100;

UX clé. Toggle "All Time / This Week" sur la page leaderboard. Compte à rebours visible "Week ends in 2d 4h".


1.6 — On-chain Tasks (Solana-native)

Description. Tasks vérifiables on-chain qui collent à l'identité crypto :

Impact. Fort. Filtre les vrais users crypto, donne de la légitimité, et c'est exactement le type de tasks attendu d'un wallet tracker.

Complexité. Moyen-complexe. Besoin d'un Solana RPC (Helius, QuickNode) côté backend pour vérifier on-chain. Déjà sur ta stack vu que tu fais SIWS.

DB. Aucun changement majeur — utilise le tasks.config existant + tasks.type = 'onchain' (déjà supporté). Stocker la preuve dans completions.proof.

-- exemple config pour task "hold 0.1 SOL"
{
  "kind": "min_balance",
  "mint": "So11111111111111111111111111111111111111112",
  "min_amount": 100000000,
  "snapshot_count": 7,
  "interval": "daily"
}

UX clé. Bouton "Verify on-chain" qui montre une animation de scan + résultat clair "Found a Jupiter swap from your wallet". Si pas trouvé : message qui aide ("No swap found in last 30 days. Try one and check back!").


1.7 — Quest "Use the Bot" (data tasks)

Description. Tasks ancrées dans le produit lui-même :

Impact. Fort. Force l'utilisation réelle du produit, pas juste du social grinding. Différenciant vs Galxe-style quests.

Complexité. Moyen. Besoin d'instrumenter le bot/tool pour logger les actions. Endpoint webhook depuis le bot vers ton API.

DB.

create table user_actions (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references users(id),
  action_kind text not null, -- 'wallet_scan', 'cross_ref', 'bot_command', 'save_wallet'
  metadata jsonb not null default '{}',
  created_at timestamptz not null default now()
);
create index idx_user_actions_user_kind on user_actions(user_id, action_kind, created_at desc);

UX clé. "Actions" tab montre un compteur live "12 / 50 wallets scanned this season". Le tracking se fait sans friction quand l'user utilise le tool normalement.


1.8 — Boost Hours (Power Hour)

Description. 1 fois par jour, une fenêtre aléatoire de 1h où tous les points gagnés sont X2. Annoncée 15 min avant via push (Telegram/X). Crée FOMO et concentre l'activité.

Impact. Fort. Adopté par Pump.fun, Hyperliquid SS2, Drift. Génère des spikes d'engagement énormes.

Complexité. Simple. Une table de "boost windows" + check dans le trigger d'attribution de points.

DB.

create table boost_windows (
  id uuid primary key default gen_random_uuid(),
  starts_at timestamptz not null,
  ends_at timestamptz not null,
  multiplier numeric(3,2) not null default 2.0,
  reason text -- 'daily_power_hour', 'weekend_boost', 'launch_special'
);
create index idx_boost_windows_active on boost_windows(starts_at, ends_at);

UX clé. Bandeau rouge en haut du dashboard "POWER HOUR! 47 min left — all points X2". Notif Telegram bot 15 min avant.


1.9 — Squad / Teams

Description. L'user crée ou rejoint une "squad" (max 10 personnes). Les pts de chaque membre comptent collectivement -> squad leaderboard hebdo. Top 3 squads = chaque membre reçoit un bonus. Crée de la coopération + pression sociale.

Impact. Fort. Mécanique clé du succès de Friend.tech, Backpack BACK, Pump.fun. Transforme les users en recruteurs internes.

Complexité. Moyen. CRUD squads + jointure. Pas de combat/loot, juste agrégation de pts.

DB.

create table squads (
  id uuid primary key default gen_random_uuid(),
  name text unique not null,
  slug text unique not null,
  emoji text,
  leader_id uuid not null references users(id),
  member_count int not null default 1,
  total_points int not null default 0,
  created_at timestamptz not null default now()
);
create table squad_members (
  squad_id uuid not null references squads(id) on delete cascade,
  user_id uuid not null unique references users(id) on delete cascade,
  joined_at timestamptz not null default now(),
  primary key (squad_id, user_id)
);
create index idx_squads_total_points on squads(total_points desc);

UX clé. Page "Squad" avec emoji custom, top 3 contributeurs, classement vs autres squads. Notif "Your squadmate @alex just hit 1000 pts!".


1.10 — Share Card / OG Image dynamique

Description. À chaque milestone (rank up, streak 7/30, top 100 leaderboard, 1000 pts), génère une OG image custom : son rank, ses stats, son tier, son squad. Partageable en 1 click sur X avec template pré-rempli "I just hit Hunter rank on @diggr_app — try it: [referral link]".

Impact. Fort. Boucle virale gratuite. C'est ce qui a fait exploser Wrapped (Spotify), Cal.com booking pages, Friend.tech profiles.

Complexité. Moyen. Edge function (Vercel/Cloudflare) qui génère du PNG via @vercel/og ou Satori. URLs cachables.

DB.

-- Aucune table requise. URL pattern : /api/og/[wallet]?milestone=streak_7
-- Optionnel : table de log pour analytics
create table share_events (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references users(id),
  kind text not null, -- 'streak_7', 'rank_up_hunter', 'top_100', etc
  shared_to text, -- 'x', 'telegram', 'copy'
  created_at timestamptz not null default now()
);

UX clé. Modal automatique "You hit a 7-day streak! Share it?" avec bouton "Tweet" pré-rempli. Tracking ciblé pour mesurer le viral coefficient.


2. Top 20 features secondaires

2.1 — First-N Bonus

Description. Les 100 premiers à compléter un certain achievement (ex: première squad de 10) reçoivent un badge permanent "OG Squad Founder #42".

Impact. Moyen. Crée urgence et FOMO sur des moments précis.

Complexité. Simple. Compteur + condition.

DB. Table achievements + user_achievements (cf. schéma consolidé).

UX. Badge avec numéro gravé sur le profil. "OG #42 — 1 of 100".


2.2 — Mystery Boxes

Description. Tous les 500 pts gagnés, l'user débloque une mystery box. Loot : 10-200 pts random, rare un NFT badge, ultra-rare un boost permanent X1.05.

Impact. Moyen. Gamble loop, dopamine, mais à modérer pour pas paraître pay-to-win.

Complexité. Moyen. Loot table + animation d'ouverture.

DB.

create table mystery_boxes (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references users(id),
  unlocked_at timestamptz not null default now(),
  opened_at timestamptz,
  reward_kind text, -- 'points', 'badge', 'multiplier'
  reward_value jsonb
);

UX. Animation 3D box qui s'ouvre, son satisfying.


2.3 — Weekly Mission

Description. Une "grosse" mission par semaine combinant 3-5 sous-tâches avec un gros reward final (200-500 pts). Ex: "Refer 2 friends + scan 10 wallets + RT pinned tweet".

Impact. Moyen-fort. Donne un cap hebdo clair.

Complexité. Simple. Réutilise le système de tasks avec un type 'mission' qui agrège des conditions.

DB.

create table missions (
  id uuid primary key default gen_random_uuid(),
  title text not null,
  conditions jsonb not null, -- [{kind:'task', task_id:...}, {kind:'refer', count:2}]
  reward_points int not null,
  starts_at timestamptz, ends_at timestamptz,
  enabled bool default true
);
create table mission_completions (user_id uuid, mission_id uuid, completed_at timestamptz, primary key(user_id, mission_id));

UX. Encart "This Week's Mission" très visible avec progress bar globale.


2.4 — Quote Tweet Tasks

Description. Au lieu d'un simple RT, des tasks "Quote Tweet avec une raison de pourquoi tu utilises diggr" (30 pts). Validation manuelle (admin) ou auto via API X + check du texte.

Impact. Moyen-fort. Génère du contenu organique de qualité, pas juste du spam RT.

Complexité. Moyen. Validation manuelle au début, automatisable plus tard.

DB. Réutiliser tasks.type = 'quote_rt'. Ajouter completions.proof.tweet_url et verified_by admin.

UX. Liste de QT en attente côté admin, modale claire côté user "We'll review and credit you within 24h".


2.5 — Captcha Quizz Crypto

Description. Avant de claim une grosse récompense, l'user passe un mini-quiz crypto/Solana (3 questions random). Bonne réponse = unlock. Mauvaises = wait 1h. Filtre les bots et farmers Asie pas natifs.

Impact. Moyen (anti-farming). Faible (engagement direct).

Complexité. Simple. Pool de questions JSON + endpoint.

DB.

create table quiz_questions (
  id serial primary key,
  question text, options jsonb, correct_index smallint, difficulty smallint
);
create table quiz_attempts (
  user_id uuid, attempted_at timestamptz, passed bool, score smallint
);

UX. Modal type Duolingo, propre, 60s timer, ludique.


2.6 — Custom Referral Link (vanity)

Description. Au-delà du slug random, l'user à 500 pts peut customiser son slug : diggr.app/r/morgan au lieu de /r/x9k4mn2p.

Impact. Moyen. Petit bénéfice de personnalisation, multiplie le partage.

Complexité. Simple. Champ + validation unicité + reserved words.

DB.

alter table users add column custom_slug text unique;
-- contraintes : 3-20 chars, [a-z0-9_], pas dans une liste réservée

UX. "Claim your name" CTA sur le profil quand atteint 500 pts.


2.7 — Friends List + Live Activity

Description. L'user peut "follow" d'autres wallets (filleuls, squad mates, leaderboard players) et voir un feed "alex.sol just completed today's RT", "kev hit 5000 pts". Crée pression sociale comme Strava.

Impact. Moyen-fort sur retention.

Complexité. Moyen. Système de follow + feed agrégé.

DB.

create table follows (
  follower_id uuid not null references users(id),
  followee_id uuid not null references users(id),
  created_at timestamptz default now(),
  primary key (follower_id, followee_id),
  check (follower_id <> followee_id)
);
-- Activity feed = query points_log filtré sur followees + types public

UX. Page "Activity" minimaliste type Twitter timeline.


2.8 — Notifications & Push (Telegram-first)

Description. Bot Telegram envoie : streak warning à 22h, power hour incoming, daily task reminder, milestone hits, leaderboard rank changes. Opt-in par user.

Impact. Fort sur retention.

Complexité. Moyen. Cron + bot API.

DB.

alter table users add column tg_notif_prefs jsonb not null default '{
  "streak_warning": true,
  "power_hour": true,
  "daily_reminder": true,
  "milestone": true,
  "rank_change": false
}'::jsonb;

UX. Page "Notifications" avec toggles. Tone : sec et utile, pas spammy.


2.9 — Soulbound Achievement NFTs

Description. À chaque milestone majeur (rank up, streak 30, top 10 weekly) -> mint d'un NFT soulbound (non transférable) sur Solana. Coût gas négligeable. Visible dans Phantom = trophée permanent.

Impact. Moyen. Tease l'airdrop sans rien promettre, badge immutable.

Complexité. Complexe. Nécessite un programme Solana ou Metaplex Core. Mais fait une fois -> réutilisable.

DB.

create table sbt_mints (
  id uuid primary key default gen_random_uuid(),
  user_id uuid not null references users(id),
  achievement_kind text not null,
  mint_address text unique,
  tx_signature text unique,
  minted_at timestamptz default now()
);

UX. "Claim your NFT badge" CTA, transaction transparente, image custom.


2.10 — Sound + Confetti Rewards

Description. Animations satisfaisantes à chaque points gain : son court + confetti subtil. Rank up = animation full screen.

Impact. Moyen. Petit boost de rétention via dopamine.

Complexité. Simple. Côté front uniquement (lib canvas-confetti, fichiers audio).

DB. Aucune.

UX. À calibrer, pas trop spammy. Toggle off dans settings.


2.11 — "Bring a Friend" Day

Description. 1 jour par mois (annoncé), tous les referrals comptent X2 (+100 au lieu de +50). Date variable pour créer FOMO.

Impact. Fort sur les jours concernés. Spike de signups.

Complexité. Simple. Réutilise le mécanisme de boost windows mais appliqué au reward referral.

DB. Réutilise boost_windows avec reason='referral_x2'.

UX. Compte à rebours géant la veille. Push à tous via Telegram.


2.12 — Capped Rewards (premiers N)

Description. "Première 1000 personnes à atteindre 500 pts -> +200 bonus permanent". Crée urgence sur le early stage.

Impact. Moyen. Très efficace sur les 2 premières semaines.

Complexité. Simple. Compteur + condition au moment du palier.

DB. Une table point_milestones avec compteur, ou simplement un trigger qui check count(users where points >= 500).

UX. "234 / 1000 spots left for First Sniper bonus". Visible en haut du dashboard.


2.13 — Seasonal Campaigns

Description. Saisons de 4-8 semaines avec thème (Halloween, Christmas, V2 Launch). Reset partiel (seasonal points distincts) + rewards exclusifs cosmétiques.

Impact. Fort à long-terme. Empêche le sentiment "trop tard pour rejoindre".

Complexité. Moyen. Refactor pour supporter "saisons" en parallèle de l'all-time.

DB.

create table seasons (
  id smallint primary key,
  name text not null,
  starts_at timestamptz not null,
  ends_at timestamptz not null,
  theme text,
  active bool default false
);
alter table points_log add column season_id smallint references seasons(id);
alter table completions add column season_id smallint references seasons(id);

UX. Theme switch global (couleurs, mascotte). Page "Season Hub".


2.14 — Profile Page Publique

Description. diggr.app/u/[slug] = page publique avec rank, streaks, badges, squad, top achievements. Indexable Google. Brag-worthy.

Impact. Moyen-fort sur viralité. Chaque user devient une mini landing.

Complexité. Simple. Page publique read-only depuis la vue déjà existante (extension de v_leaderboard).

DB.

create or replace view v_public_profile as
select id, custom_slug, x_username, points, current_streak, longest_streak,
  tier_id, created_at, /* ... */
from users where points > 0;
grant select on v_public_profile to anon;

UX. Page propre, dark mode, OG image auto, bouton "Beat me — join with my link".


2.15 — Inverse Leaderboard "Diggers Sleeping"

Description. Page secondaire qui montre les wallets en risque de perdre leur streak (>20h depuis last action). Permet aux squad mates de pinger leurs amis.

Impact. Moyen. Original, force la communauté à se réveiller.

Complexité. Simple. Query basée sur last_streak_day.

DB. Aucune nouvelle table.

UX. "12 of your squadmates haven't done today's task. Wake them up?" -> bouton qui send DM Telegram.


2.16 — Referral Competition Hebdo

Description. Top 10 recruteurs de la semaine = 200/100/50 pts + "Recruiter of the Week" badge.

Impact. Fort. Aligne incitatif viral et compétition.

Complexité. Simple. Vue agrégée de referrals filtrée par semaine.

DB.

create materialized view mv_weekly_referrers as
select referrer_id, count(*) as referrals_this_week
from referrals
where created_at >= date_trunc('week', now()) and reward_credited = true
group by referrer_id order by referrals_this_week desc;

UX. Onglet dédié sur la page leaderboard.


2.17 — "Predict the Token" mini-game

Description. Une fois par semaine, mini-game : "Quelle sera la prochaine memecoin top trending sur Solana ce vendredi ?". User pick 1 token dans une liste curée. Bonne réponse = +30 pts. Lien naturel avec le produit (wallet tracking).

Impact. Moyen. Différenciant, on-brand.

Complexité. Moyen. Curation manuelle hebdo + résolution + check on-chain.

DB.

create table predictions (
  id uuid primary key default gen_random_uuid(),
  week_start date,
  options jsonb, -- liste de {symbol, address}
  correct_option text, resolved_at timestamptz
);
create table user_predictions (
  user_id uuid, prediction_id uuid, choice text, submitted_at timestamptz,
  primary key(user_id, prediction_id)
);

UX. Encart "Your prediction this week" sur dashboard, vendredi soir -> réveil avec résultats.


2.18 — Wallet Age Multiplier

Description. Les wallets >365 jours d'activité on-chain (vérifié au signup) reçoivent un X1.1 permanent. Wallet flambant neuf = X1.0. Anti-farm élégant.

Impact. Moyen (anti-farm). Faible (engagement direct).

Complexité. Moyen. RPC call pour récupérer l'âge du wallet.

DB.

alter table users add column wallet_age_days int;
alter table users add column permanent_multiplier numeric(3,2) not null default 1.0;

UX. Affiché dans le profil "Your wallet: 412 days old -> X1.1 permanent boost".


2.19 — Achievement Tree

Description. Page "Achievements" type Steam : grille de 30-50 badges déblocables. Chaque badge = condition spécifique (refer 5, streak 14, complete 100 tasks, scan 50 wallets…). Badges = +5 à +100 pts chacun + cosmétique.

Impact. Moyen-fort. Donne 50+ micro-objectifs aux completionists.

Complexité. Moyen. Système générique + définition JSON des conditions.

DB.

create table achievements (
  id text primary key, -- 'refer_5', 'streak_14'
  name text, description text, icon_url text,
  condition jsonb not null, points_reward int default 0,
  rarity text -- 'common','rare','epic','legendary'
);
create table user_achievements (
  user_id uuid, achievement_id text,
  unlocked_at timestamptz default now(),
  primary key(user_id, achievement_id)
);

UX. Grid avec badges grisés/colorés. Hover = condition d'unlock. Filtres par rareté.


2.20 — Timed Challenges Pop-up

Description. Aléatoirement, 2x par semaine, un challenge surprise apparaît : "Complete 3 tasks in the next 30 min for X2". Push notif, timer visible.

Impact. Moyen. Spike d'activité, FOMO.

Complexité. Moyen. Cron + state temporaire par user.

DB.

create table flash_challenges (
  id uuid primary key default gen_random_uuid(),
  user_id uuid references users(id), -- null = global
  starts_at timestamptz, ends_at timestamptz,
  conditions jsonb, reward jsonb,
  completed bool default false
);

UX. Toast bottom-right qui pulse, timer rouge.


3. Idées audacieuses / différenciantes

Ces idées rendent diggr unique vs n'importe quelle copy-paste de Galxe. Elles tirent parti du fait que diggr EST un outil de wallet tracking — l'identité du produit doit transpirer dans le gameplay.


3.1 — "Diggr Hunt" weekly challenge (signature)

Description. Chaque lundi, l'admin publie un wallet "mystère" (un vrai wallet smart-money). L'user doit utiliser le tool diggr pour identifier 3 contracts que ce wallet a touchés en premier. Soumet la réponse -> +200 pts au premier qui trouve, +50 aux 100 suivants. C'est LE puzzle de la semaine.

Pourquoi ça marche. C'est exactement ce que fait l'outil. Ça transforme le tool en jeu. Le résultat, c'est que les users apprennent à utiliser diggr en profondeur tout en se challengeant. Différenciant total.

Impact. Très fort. Engagement + product education + organic content (les users vont tweeter leurs trouvailles).

Complexité. Moyen. Curation hebdo + endpoint de soumission + scoring.

DB.

create table hunts (
  id uuid primary key default gen_random_uuid(),
  week_start date unique,
  target_wallet text not null,
  expected_contracts text[] not null, -- 3 addresses (ordre libre)
  difficulty smallint, points_first int default 200, points_runner_up int default 50,
  starts_at timestamptz, ends_at timestamptz
);
create table hunt_submissions (
  id uuid primary key default gen_random_uuid(),
  hunt_id uuid references hunts(id), user_id uuid references users(id),
  contracts text[] not null, submitted_at timestamptz default now(),
  is_correct bool, position int -- 1st, 2nd...
);

UX. Page dédiée "Hunt of the Week" avec hint text, formulaire, leaderboard live des solveurs.


3.2 — Wallet Score "Reputation"

Description. Chaque user a un Diggr Score (0-1000) calculé à partir de : age wallet + activité on-chain + completion ratio + ancienneté plateforme + diversity actions. Score visible publiquement = signal de "real digger" vs farmer. Pondère certains rewards.

Pourquoi ça marche. Ça donne une métrique qualitative, pas juste quantitative. Comme le "GitHub contribution graph" version crypto. Hyperliquid a fait quelque chose de similaire avec leur points system pondéré par PnL/trading patterns.

Impact. Fort à long-terme.

Complexité. Complexe. Nécessite un scoring engine.

DB.

alter table users add column diggr_score int default 0;
alter table users add column score_components jsonb default '{}';
-- recalculé via cron daily

UX. Une jauge circulaire sur le profil, breakdown visible.


3.3 — Token Battles entre Squads

Description. Chaque semaine, 2 squads "ennemies" sont matchées par leaderboard adjacent. La squad qui gagne le plus de pts pendant la semaine "vole" 10% des pts de l'ennemie sur le scoreboard saisonnier. Crée des rivalités épiques.

Pourquoi ça marche. Friend.tech, Pump.fun "communities", Backpack BACK ont prouvé que la mécanique "vs" crée un engagement social explosif. Les users tagent leurs ennemis sur X, font des memes.

Impact. Très fort sur viralité.

Complexité. Complexe. Matchmaking + résolution + animations de "raid".

DB.

create table squad_battles (
  id uuid primary key default gen_random_uuid(),
  week_start date,
  squad_a uuid references squads(id), squad_b uuid references squads(id),
  score_a int default 0, score_b int default 0,
  winner_id uuid, points_stolen int, resolved_at timestamptz
);

UX. Page "Battles" avec animation de combat (juste UI), trash-talk chat dans la squad.


3.4 — Live "Whale Sighting" feed

Description. Quand un wallet en leaderboard top 10 utilise le tool pour scanner un wallet inhabituel, ce scan apparaît anonymisé dans un feed live "A Diamond Diggr just scanned wallet 0x...". Crée un sentiment de "alpha leakage" et fait perdre du temps aux users sur le site.

Pourquoi ça marche. Joue sur le voyeurisme natif de la communauté wallet-tracking. Les gens vont vouloir découvrir ce que les top users regardent.

Impact. Fort sur time-on-site. Moyen sur acquisition.

Complexité. Moyen. Feed Redis-style + filtre privacy.

DB. Réutilise user_actions. Vue filtrée publique.

UX. Sidebar live qui scroll en continu, font monospace, hacker-vibes.


3.5 — Anonymous "Snitch" referral

Description. Permettre à un user de "snitch" sur un autre wallet farmer (multi-wallets, pattern bot, etc.). Si l'admin confirme = ban + redistribution des pts du fraudeur, dont 10% au snitcher. Anti-farm crowdsourcé.

Pourquoi ça marche. Convertit les farmers victimes (vrais users) en alliés actifs de la modération. Hyperliquid a un système similaire avec les "bag-holders" qui flag les bots.

Impact. Moyen. Excellent pour la perception de fairness.

Complexité. Moyen. UI + workflow admin.

DB.

create table reports (
  id uuid primary key default gen_random_uuid(),
  reporter_id uuid references users(id),
  reported_user_id uuid references users(id),
  reason text, evidence jsonb,
  status text default 'pending', -- pending/confirmed/rejected
  resolved_at timestamptz, resolved_by_admin_id uuid,
  reward_credited bool default false
);

UX. Flag button discret sur leaderboard. Promesse "Your report stays anonymous".


3.6 — "Calendar Heatmap" GitHub-style

Description. Un calendrier annuel sur le profil qui montre l'activité jour par jour (vert plus intense = plus de pts). Comme GitHub contributions. Très brag-worthy.

Pourquoi ça marche. Format mental déjà connu et adoré. Très engageant à completer.

Impact. Moyen-fort sur retention long-terme.

Complexité. Simple. Query agrégée de points_log group by date.

DB. Aucune.

UX. Component heatmap classique, hover sur case = détail.


3.7 — "Onchain Proof of Diggr" - SBT graduation

Description. Quand un user atteint le top tier (Diamond Diggr), il reçoit un NFT SBT signé par le treasury wallet officiel. Ce NFT devient un pré-requis pour des features V2 du tool (alertes whale custom, API access). Pas un airdrop de token, mais une garantie d'utility.

Pourquoi ça marche. Sans rien promettre légalement, ça aligne incitatifs et donne une vraie valeur on-chain. Les top users sont fidélisés à vie.

Impact. Fort.

Complexité. Complexe (smart contract + mint + frontend integration).

DB. cf 2.9 sbt_mints.

UX. Cérémonie visuelle de mint, share automatique sur X.


4. Schéma DB consolidé (Postgres)

À appliquer en migrations incrémentales. Chaque bloc est independent — tu peux ne shipper que les features que tu veux.

-- =============================================================
-- DIGGR — Gamification migrations
-- À appliquer après 001_initial_schema.sql
-- =============================================================

-- ========== USERS extensions ==========
alter table public.users add column if not exists current_streak int not null default 0;
alter table public.users add column if not exists longest_streak int not null default 0;
alter table public.users add column if not exists last_streak_day date;
alter table public.users add column if not exists daily_multiplier numeric(3,2) not null default 1.0;
alter table public.users add column if not exists combo_streak int not null default 0;
alter table public.users add column if not exists tier_id smallint;
alter table public.users add column if not exists wallet_age_days int;
alter table public.users add column if not exists permanent_multiplier numeric(3,2) not null default 1.0;
alter table public.users add column if not exists diggr_score int not null default 0;
alter table public.users add column if not exists score_components jsonb not null default '{}';
alter table public.users add column if not exists custom_slug text unique;
alter table public.users add column if not exists tg_notif_prefs jsonb not null default '{
  "streak_warning": true, "power_hour": true, "daily_reminder": true,
  "milestone": true, "rank_change": false
}'::jsonb;

create index if not exists idx_users_diggr_score on public.users(diggr_score desc);
create index if not exists idx_users_custom_slug on public.users(custom_slug) where custom_slug is not null;

-- ========== TIERS ==========
create table if not exists public.tiers (
  id          smallint primary key,
  name        text not null,
  slug        text unique not null,
  min_points  int not null,
  emoji       text,
  perks       jsonb not null default '{}'
);

insert into public.tiers (id, name, slug, min_points, emoji, perks) values
  (1, 'Pleb',           'pleb',           0,      'seedling',       '{}'),
  (2, 'Scout',          'scout',          250,    'telescope',      '{"daily_task_slots": 1}'),
  (3, 'Tracker',        'tracker',        1000,   'satellite',      '{"daily_task_slots": 2, "custom_slug": true}'),
  (4, 'Hunter',         'hunter',         5000,   'target',         '{"beta_access": true}'),
  (5, 'Whale-Watcher',  'whale_watcher',  15000,  'whale',          '{"alpha_channel": true}'),
  (6, 'Diamond Diggr',  'diamond_diggr',  50000,  'diamond',        '{"sbt_eligible": true, "v2_early": true}')
on conflict (id) do nothing;

-- ========== REFERRALS extensions (multi-tier) ==========
alter table public.referrals add column if not exists tier_level smallint not null default 1;
alter table public.referrals add column if not exists root_referrer_id uuid references public.users(id);
create index if not exists idx_referrals_root on public.referrals(root_referrer_id);

-- ========== BOOST WINDOWS ==========
create table if not exists public.boost_windows (
  id          uuid primary key default gen_random_uuid(),
  starts_at   timestamptz not null,
  ends_at     timestamptz not null,
  multiplier  numeric(3,2) not null default 2.0,
  reason      text not null, -- 'power_hour', 'weekend_x2', 'referral_x2', 'launch'
  applies_to  text not null default 'all', -- 'all' | 'tasks' | 'referrals'
  created_at  timestamptz not null default now()
);
create index if not exists idx_boost_active on public.boost_windows(starts_at, ends_at);

-- ========== USER ACTIONS (product instrumentation) ==========
create table if not exists public.user_actions (
  id          uuid primary key default gen_random_uuid(),
  user_id     uuid not null references public.users(id) on delete cascade,
  action_kind text not null,  -- 'wallet_scan', 'cross_ref', 'bot_command', 'save_wallet'
  metadata    jsonb not null default '{}',
  created_at  timestamptz not null default now()
);
create index if not exists idx_user_actions on public.user_actions(user_id, action_kind, created_at desc);

-- ========== SQUADS ==========
create table if not exists public.squads (
  id            uuid primary key default gen_random_uuid(),
  name          text unique not null,
  slug          text unique not null,
  emoji         text,
  description   text,
  leader_id     uuid not null references public.users(id),
  member_count  int not null default 1,
  total_points  int not null default 0,
  weekly_points int not null default 0,
  created_at    timestamptz not null default now()
);
create table if not exists public.squad_members (
  squad_id  uuid not null references public.squads(id) on delete cascade,
  user_id   uuid not null unique references public.users(id) on delete cascade,
  joined_at timestamptz not null default now(),
  primary key (squad_id, user_id)
);
create index if not exists idx_squads_total on public.squads(total_points desc);
create index if not exists idx_squads_weekly on public.squads(weekly_points desc);

-- ========== WEEKLY LEADERBOARD ==========
create table if not exists public.weekly_leaderboard_history (
  week_start      date not null,
  user_id         uuid not null references public.users(id) on delete cascade,
  rank            int not null,
  weekly_points   int not null,
  bonus_awarded   int not null default 0,
  primary key (week_start, user_id)
);

-- ========== ACHIEVEMENTS ==========
create table if not exists public.achievements (
  id              text primary key, -- 'refer_5', 'streak_14', 'og_100'
  name            text not null,
  description     text,
  icon_url        text,
  rarity          text not null check (rarity in ('common','rare','epic','legendary')),
  condition       jsonb not null,
  points_reward   int not null default 0,
  is_capped       bool not null default false, -- true = first N seulement
  cap_count       int,
  current_count   int not null default 0,
  enabled         bool not null default true,
  created_at      timestamptz not null default now()
);
create table if not exists public.user_achievements (
  user_id         uuid not null references public.users(id) on delete cascade,
  achievement_id  text not null references public.achievements(id),
  unlocked_at     timestamptz not null default now(),
  ordinal         int, -- pour les capped : "OG #42"
  primary key (user_id, achievement_id)
);
create index if not exists idx_user_ach_user on public.user_achievements(user_id);

-- ========== MISSIONS (weekly) ==========
create table if not exists public.missions (
  id              uuid primary key default gen_random_uuid(),
  title           text not null,
  description     text,
  conditions      jsonb not null, -- liste de conditions
  reward_points   int not null,
  starts_at       timestamptz, ends_at timestamptz,
  enabled         bool not null default true
);
create table if not exists public.mission_completions (
  user_id         uuid not null references public.users(id) on delete cascade,
  mission_id      uuid not null references public.missions(id) on delete cascade,
  completed_at    timestamptz not null default now(),
  primary key (user_id, mission_id)
);

-- ========== HUNTS (signature feature) ==========
create table if not exists public.hunts (
  id                  uuid primary key default gen_random_uuid(),
  week_start          date unique,
  target_wallet       text not null,
  expected_contracts  text[] not null,
  hint                text,
  difficulty          smallint default 1,
  points_first        int default 200,
  points_runner_up    int default 50,
  starts_at           timestamptz, ends_at timestamptz,
  resolved            bool default false
);
create table if not exists public.hunt_submissions (
  id            uuid primary key default gen_random_uuid(),
  hunt_id       uuid not null references public.hunts(id) on delete cascade,
  user_id       uuid not null references public.users(id) on delete cascade,
  contracts     text[] not null,
  submitted_at  timestamptz not null default now(),
  is_correct    bool,
  position      int,
  unique(hunt_id, user_id)
);

-- ========== FOLLOWS ==========
create table if not exists public.follows (
  follower_id   uuid not null references public.users(id) on delete cascade,
  followee_id   uuid not null references public.users(id) on delete cascade,
  created_at    timestamptz not null default now(),
  primary key (follower_id, followee_id),
  check (follower_id <> followee_id)
);

-- ========== MYSTERY BOXES ==========
create table if not exists public.mystery_boxes (
  id            uuid primary key default gen_random_uuid(),
  user_id       uuid not null references public.users(id) on delete cascade,
  unlocked_at   timestamptz not null default now(),
  opened_at     timestamptz,
  reward_kind   text, -- 'points','badge','multiplier'
  reward_value  jsonb
);

-- ========== SEASONS ==========
create table if not exists public.seasons (
  id          smallint primary key,
  name        text not null,
  starts_at   timestamptz not null,
  ends_at     timestamptz not null,
  theme       text,
  active      bool not null default false
);
alter table public.points_log add column if not exists season_id smallint references public.seasons(id);
alter table public.completions add column if not exists season_id smallint references public.seasons(id);

-- ========== SHARE EVENTS (analytics viral) ==========
create table if not exists public.share_events (
  id          uuid primary key default gen_random_uuid(),
  user_id     uuid not null references public.users(id) on delete cascade,
  kind        text not null,
  shared_to   text,
  created_at  timestamptz not null default now()
);

-- ========== REPORTS (anti-farm) ==========
create table if not exists public.reports (
  id                  uuid primary key default gen_random_uuid(),
  reporter_id         uuid references public.users(id),
  reported_user_id    uuid references public.users(id),
  reason              text not null,
  evidence            jsonb,
  status              text not null default 'pending',
  resolved_at         timestamptz,
  resolved_by_admin   uuid,
  reward_credited     bool not null default false,
  created_at          timestamptz not null default now()
);

-- ========== SBT MINTS ==========
create table if not exists public.sbt_mints (
  id                uuid primary key default gen_random_uuid(),
  user_id           uuid not null references public.users(id) on delete cascade,
  achievement_kind  text not null,
  mint_address      text unique,
  tx_signature      text unique,
  minted_at         timestamptz not null default now()
);

-- ========== RLS lockdown ==========
alter table public.boost_windows         enable row level security;
alter table public.user_actions          enable row level security;
alter table public.squads                enable row level security;
alter table public.squad_members         enable row level security;
alter table public.weekly_leaderboard_history enable row level security;
alter table public.achievements          enable row level security;
alter table public.user_achievements     enable row level security;
alter table public.missions              enable row level security;
alter table public.mission_completions   enable row level security;
alter table public.hunts                 enable row level security;
alter table public.hunt_submissions      enable row level security;
alter table public.follows               enable row level security;
alter table public.mystery_boxes         enable row level security;
alter table public.seasons               enable row level security;
alter table public.share_events          enable row level security;
alter table public.reports               enable row level security;
alter table public.sbt_mints             enable row level security;
alter table public.tiers                 enable row level security;

-- Read policies pour les tables publiques (config)
drop policy if exists "tiers_read" on public.tiers;
create policy "tiers_read" on public.tiers for select using (true);

drop policy if exists "achievements_read" on public.achievements;
create policy "achievements_read" on public.achievements for select using (enabled = true);

drop policy if exists "boost_active_read" on public.boost_windows;
create policy "boost_active_read" on public.boost_windows for select
  using (starts_at <= now() and ends_at >= now());

drop policy if exists "squads_read" on public.squads;
create policy "squads_read" on public.squads for select using (true);

drop policy if exists "seasons_read_active" on public.seasons;
create policy "seasons_read_active" on public.seasons for select using (active = true);

drop policy if exists "hunts_read_current" on public.hunts;
create policy "hunts_read_current" on public.hunts for select
  using (starts_at <= now() and (ends_at is null or ends_at >= now() - interval '7 days'));

-- Tout le reste (writes) passe par service_role => RLS bloque par défaut.

-- ========== TRIGGER étendu : awarding avec multipliers + boost ==========
-- Remplace fn_log_points_on_completion
create or replace function public.fn_log_points_on_completion()
returns trigger language plpgsql security definer as $$
declare
  v_recurrence text;
  v_existing int;
  v_user_mult numeric(3,2);
  v_perm_mult numeric(3,2);
  v_boost_mult numeric(3,2) := 1.0;
  v_final_points int;
  v_new_balance int;
  v_today date := (now() at time zone 'UTC')::date;
  v_last_day date;
begin
  -- Anti-doublon (existing logic)
  select recurrence into v_recurrence from public.tasks where id = new.task_id;

  if v_recurrence = 'daily' then
    select count(*) into v_existing from public.completions
    where user_id = new.user_id and task_id = new.task_id
      and (completed_at at time zone 'UTC')::date = v_today
      and id <> new.id;
    if v_existing > 0 then
      raise exception 'Daily task already completed today' using errcode='unique_violation';
    end if;
  elsif v_recurrence = 'once' then
    select count(*) into v_existing from public.completions
    where user_id = new.user_id and task_id = new.task_id and id <> new.id;
    if v_existing > 0 then
      raise exception 'Task already completed' using errcode='unique_violation';
    end if;
  end if;

  -- Multipliers
  select coalesce(daily_multiplier, 1.0), coalesce(permanent_multiplier, 1.0), last_streak_day
    into v_user_mult, v_perm_mult, v_last_day
  from public.users where id = new.user_id;

  select coalesce(max(multiplier), 1.0) into v_boost_mult
  from public.boost_windows
  where starts_at <= now() and ends_at >= now()
    and applies_to in ('all','tasks');

  -- Final points
  v_final_points := round(new.points_awarded * v_user_mult * v_perm_mult * v_boost_mult);

  -- Update user balance + streak
  update public.users
     set points = points + v_final_points,
         current_streak = case
           when v_last_day = v_today then current_streak  -- déjà compté today
           when v_last_day = v_today - 1 then current_streak + 1
           else 1
         end,
         longest_streak = greatest(longest_streak,
           case
             when v_last_day = v_today then current_streak
             when v_last_day = v_today - 1 then current_streak + 1
             else 1
           end),
         last_streak_day = v_today
   where id = new.user_id
   returning points into v_new_balance;

  -- Override le points_awarded enregistré pour audit
  new.points_awarded := v_final_points;

  insert into public.points_log(user_id, delta, balance_after, reason, ref_id)
  values (new.user_id, v_final_points, v_new_balance, 'task:' || new.task_id::text, new.id);

  return new;
end;
$$;

-- ========== FONCTION : recompute multi-tier referral reward ==========
create or replace function public.fn_award_multi_tier_referral(p_root uuid, p_reward_l1 int default 50)
returns void language plpgsql security definer as $$
declare
  v_l2_referrer uuid;
  v_l3_referrer uuid;
  v_b int;
begin
  -- L1 = p_root (déjà géré dans fn_check_referral_reward existant)

  -- L2 : qui a référé p_root ?
  select referrer_id into v_l2_referrer from public.referrals where referee_id = p_root;
  if v_l2_referrer is not null then
    update public.users set points = points + 10 where id = v_l2_referrer returning points into v_b;
    insert into public.points_log(user_id, delta, balance_after, reason, ref_id)
    values (v_l2_referrer, 10, v_b, 'referral_l2', p_root);

    -- L3 : qui a référé v_l2_referrer ?
    select referrer_id into v_l3_referrer from public.referrals where referee_id = v_l2_referrer;
    if v_l3_referrer is not null then
      update public.users set points = points + 2 where id = v_l3_referrer returning points into v_b;
      insert into public.points_log(user_id, delta, balance_after, reason, ref_id)
      values (v_l3_referrer, 2, v_b, 'referral_l3', p_root);
    end if;
  end if;
end;
$$;
-- À appeler depuis fn_check_referral_reward après le crédit L1.

-- ========== VUE : tier dérivé ==========
create or replace view public.v_user_tier as
select u.id as user_id, u.points,
  (select t.id from public.tiers t where u.points >= t.min_points order by t.min_points desc limit 1) as tier_id,
  (select t.name from public.tiers t where u.points >= t.min_points order by t.min_points desc limit 1) as tier_name
from public.users u;
grant select on public.v_user_tier to anon, authenticated;

5. Roadmap suggérée — Solo dev

Approche : valider l'engagement avant d'investir dans des features lourdes. Chaque sprint = ~1 semaine.

Phase 0 — Polish base (déjà 80% fait)

Phase 1 — "Stickiness" (Semaine 1-2) — MVP gamification

Objectif : faire revenir les users tous les jours.

  1. Daily Streak (1.1) — 1 jour de dev
  2. Tier System (1.3) — 1 jour
  3. Daily Combo Multiplier (1.2) — 2 jours
  4. Power Hour boost windows (1.8) — 1 jour
  5. Notifications Telegram basiques (2.8) — 2 jours

Critère succès : DAU > 30% des signups après 7 jours, retention D7 > 25%.


Phase 2 — "Virality" (Semaine 3-4)

Objectif : transformer chaque user en recruteur.

  1. Multi-tier referrals (1.4) — 2 jours
  2. Share Card / OG image (1.10) — 2 jours
  3. Weekly leaderboard (1.5) — 2 jours
  4. Referral Competition Hebdo (2.16) — 1 jour
  5. Profile page publique (2.14) — 1 jour

Critère succès : viral coefficient (k-factor) > 0.5, idéal > 1.


Phase 3 — "Product-fit gamification" (Semaine 5-6)

Objectif : ancrer le gameplay dans le produit lui-même.

  1. On-chain Tasks (1.6) — 3 jours
  2. Bot/Tool Usage Quests (1.7) — 2 jours
  3. Hunt of the Week (3.1) — la signature feature, 3 jours

Critère succès : >40% des users ont fait au moins 1 task on-chain ou 1 hunt.


Phase 4 — "Community" (Semaine 7-8)

Objectif : créer du social & coopération.

  1. Squads (1.9) — 4 jours
  2. Friends/Follows + activity feed (2.7) — 2 jours
  3. Achievement tree (2.19) — 3 jours

Critère succès : >30% des actifs sont en squad, partages X/jour multipliés par 2.


Phase 5 — "Long game" (Semaine 9-12)

  1. Seasons system (2.13)
  2. Soulbound NFT badges (2.9 / 3.7)
  3. Diggr Score reputation (3.2)
  4. Custom slug (2.6)
  5. Mystery boxes (2.2)

Phase 6 — "Differentiator polish"

  1. Squad Battles (3.3)
  2. Whale Sighting feed (3.4)
  3. Anti-farm Snitch (3.5)
  4. Calendar Heatmap (3.6)

6. Anti-farming layer (transversal)

À implémenter en couche dès le début, pas après — sinon tu te retrouves avec un leaderboard pourri impossible à nettoyer.

Mécanisme Effort Quand
Wallet age check au signup (+ multiplicateur permanent) Faible Phase 1
Rate-limit par IP / par device fingerprint Faible Phase 1
Cooldown entre 2 referrals (1 max / 4h) Faible Phase 1
Wallet age requirement progressif (rank Hunter+ = wallet >180j) Faible Phase 2
Captcha gamifié sur les big claims Moyen Phase 3
Quizz crypto à chaque rank up Moyen Phase 3
Détection patterns (cluster de wallets liés on-chain) Complexe Phase 5
Snitch crowdsourcé Moyen Phase 6

7. Notes de calibration des points

Le piège classique = inflation. Solution : tout exprimer en % du 1er-tier pour garder cohérence.

Action Points base Justif
Follow X/TG (base task, once) 20 Existant
Daily like/RT 10 Existant
On-chain task (Jupiter swap, hold) 30-50 Plus de friction = mérite plus
Squad battle win 100 (par membre) Coop = boost
Hunt of the week (1st) 200 Skill-based
Hunt of the week (top 100) 50
Streak 7 milestone 50
Streak 30 milestone 200
Streak 100 milestone 1000
Referral L1 50 Existant
Referral L2 10
Referral L3 2
Top 1 weekly leaderboard 200
Top 1-3 / 4-10 weekly 100 / 50
Mystery box (avg) ~30 Loot table biaisée
Achievement unlock (avg) 25

Recommandation : garder le total "earnable in 30 days" pour un user actif sans referrals autour de 3000-5000 pts. Tier 5000 (Hunter) = atteignable mais non trivial.


Annexe — Quelques principes UX qui font la différence

  1. Numbers should always go up. Jamais de soustraction sauf si c'est une mécanique-clé (battle).
  2. Show progress, not just state. "1,200 / 5,000 to Hunter" >>> "You are Tracker".
  3. Surprise rewards beat predictable ones. Mystery boxes, random power hours.
  4. Social proof everywhere. "234 users hit 1k pts today", "Your squad is rank #4".
  5. Loss aversion > gain seeking. Streak warnings, "you'll lose your X1.5 in 3h".
  6. Onboarding = first 5 minutes determine everything. Showcase la première progression dans les 2 min après signup (un easy task qui donne des pts immédiats + animation).
  7. Make sharing default, not opt-in. Toggle "share to X" pre-checked sur les milestones.

Ce qu'il NE FAUT PAS faire