You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As Paycrest, I want referral rewards and BlockFest cashback to be granted once per verified person rather than once per wallet address, so that one human cannot multiply their rewards by holding several wallets.
This is the remaining prerequisite before Privy wallets could ever be exempted from phone/ID uniqueness the way injected wallets now are (#639, merged in #640).
Acceptance Criteria
GIVEN several wallets sharing one verified phone or ID document WHEN they submit referral codes THEN only the first submission is accepted — the rest are refused as already-referred, exactly as a repeat submission on a single wallet is today
GIVEN the same group of wallets WHEN they claim BlockFest cashback THENMAX_CASHBACK_PER_WALLET and MAX_CLAIMS_PER_WALLET apply across the whole group, not per address
GIVEN a wallet that is the only one on its identity WHEN it uses referrals or cashback THEN behaviour and copy are unchanged from today
GIVEN a wallet with no verified identity (tier 0) WHEN eligibility is evaluated THEN it falls back to per-wallet scope, since there is no identity to pool on
Tech Details
Why now.feat(kyc): pool limits per identity, exempt injected wallets from uniqueness #640 moved spend limits to the verified identity via resolveIdentityScope() (app/lib/kyc-identity.ts), which is what made it safe to exempt injected wallets from the uniqueness indexes. That reasoning does not extend to Privy wallets, because the shared spend pool bounds spend only — rewards are still keyed per wallet address, and nothing pools them.
Referral submission — supabase/migrations/add_referrals_referred_wallet_unique.sql enforces referrals_referred_wallet_address_unique on lower(referred_wallet_address), i.e. one referral per wallet. app/api/referral/submit/route.ts:143 inserts keyed on the wallet and :19-21,157 map the 23505 to "You have already used a referral code". N wallets on one identity means N accepted referrals.
Referral rewards — app/api/referral/claim/route.ts keys qualification per wallet throughout: referrer_rewards_unlocked_at is read and written per wallet_address (:181-182, :197, :226-227), referral counting uses referrer_wallet_address (:204), and the KYC/volume qualification check uses :248.
BlockFest cashback — app/api/blockfest/cashback/route.ts:20-22 defines MAX_CASHBACK_PER_TRANSACTION = 100, MAX_CASHBACK_PER_WALLET = 500, MAX_CLAIMS_PER_WALLET = 10, all enforced by counting rows where wallet_address is the caller (:256-281). blockfest_cashback_claims.wallet_address carries only a plain index (idx_cashback_claims_wallet) — no unique constraint — so the caps are purely application-side and per address.
Proposed approach. Reuse resolveIdentityScope() rather than inventing a second notion of identity. Replace .eq("wallet_address", caller) with .in("wallet_address", scope.wallets) in the eligibility counts, mirroring what app/api/kyc/transaction-summary/route.ts already does for spend. For referral submission, the wallet-level unique index is not sufficient on its own — either add an identity column to referrals maintained on insert, or gate on an identity-scoped pre-check plus keep the existing index as the per-wallet backstop.
Fail closed.resolveIdentityScope() throws rather than narrowing on a database error, for the same reason it does on the spend path: a narrower scope would silently grant extra rewards. Callers must not fall back to per-wallet on failure.
Concurrency. Reward granting needs the same treatment the spend path got in insert_swap_transaction_if_within_limit — without an identity-scoped lock, two sibling wallets can both pass an eligibility check and both be granted. Consider taking advisory locks on scope.identityKeys in sorted order, as that RPC does.
Notes/Assumptions
Injected wallets are not affected by the referral half: referrals are already disabled for them (app/components/MainPageContent.tsx:441 bails on isInjectedWallet, app/components/wallet-mobile-modal/WalletView.tsx:462 hides the UI). That asymmetry is precisely why exempting injected wallets from uniqueness in feat(kyc): pool limits per identity, exempt injected wallets from uniqueness #640 opened no rewards vector, while exempting Privy wallets would.
This ticket does not propose exempting Privy wallets from uniqueness. It removes the blocker so that becomes a separate, deliberate decision.
Existing data may already contain multiple wallets per identity that have each claimed. A backfill/reconciliation policy for historical over-grants is an open question below.
Open Questions
Should historical over-grants be reconciled, or should identity scoping apply only from the deploy forward? Retroactive clawback of already-paid rewards is likely not worth it, but the counts (MAX_CLAIMS_PER_WALLET) would start from an already-consumed budget for affected users either way.
For referral submission, is an identity column on referrals (DB-enforced, stronger) preferred over an application pre-check (cheaper, racy without a lock)?
Do any other per-wallet grants need the same treatment? blockfest_participants and the play/fantasy tables were not audited as part of this scoping.
User Story
Acceptance Criteria
Tech Details
Notes/Assumptions
Open Questions