EarnOS · production · critical fixes

87 open findings.
5 patterns.

These are not 87 unrelated bugs. They are a handful of defect patterns repeated across a dozen surfaces — and the largest, by a wide margin, is the app telling users they have nothing when a request actually failed. Ordered by the Atlas's own LoopScore: hazards first, then value per unit of effort, with the viral loop weighted up. Everything here is verified against origin/main 529eb97d5. The route ledger was last scanned at 200ccb80e, so coverage counts reflect a slightly newer tree than the findings do.

87open
17P1
2hazards
32state something untrue
12in the next cut
7routes still dark

00Ship these first — irreversible harm

Two findings can cost a user something they cannot get back. They outrank everything else regardless of how cheap the alternatives are.

1 18.0 Account deletion auto-executes on a timer
P1 Trust effort S hazard Profile & settings
One tap on 'Continue to delete account' starts a 10s countdown that automatically calls the delete mutation at 0 unless the user taps the same button (now 'Cancel (n)') in time. This inverts the standard confirm pattern — inaction destroys the account — and there is no re-auth or typed confirmation for an irreversible, funds-bearing accou
Fix. Make the countdown unlock a final explicit 'Delete permanently' button instead of firing the mutation itself; consider requiring OTP re-auth when a wallet balance exists.
apps/mobile/app/(app)/profile/settings/index.tsx
2 13.1 Sub-$1 earnings can expire while claim is locked
P1 Trust effort M hazard Track referral earnings
Unclaimed rows show 'Expires in Xd' (referral-activity-row.tsx) but the claim button is hard-gated at $1 ('Reach $1 to claim'), so a user with e.g. $0.60 can only watch their money count down and vanish. No copy on the Rewards screen explains the expiry policy — the referral terms link only exists on the separate invite explainer.
Fix. Pause the expiry clock while the balance is below the $1 minimum (or allow a below-minimum claim once expiry is near), and add a one-line expiry explainer plus a pre-expiry nudge on the Rewards view.
apps/mobile/src/components/referrals/referral-claim-button-label.ts

01A failed request tells the user they have nothing · 18 findings

The single most repeated defect in the app. A query fails, and instead of an error the screen renders its empty state — “No missions”, “No rewards yet”, “No activity yet”, “$0.00”. The user is not told something went wrong; they are told, falsely, that they have nothing. On a flaky network a brand-new user is informed there is nothing to earn, nobody to invite and no money in their account, with no retry in sight.

This is a trust defect wearing a resilience costume. An error the user can see and retry costs a moment of friction. An empty state they believe costs the account.
1 68.3 Wallet hero shows "$0.00 Available earnings balance" when the stats fetch fails
P1 Trust effort S says something untrue Wallet home & activity
The wallet tab's headline number is rendered unconditionally from `useProfileStats`. That hook returns `balance = stats?.balances?.wallet ?? ZERO`, so a failed `profile.me.getMyStats` request yields ZERO, and the screen prints a confident, fully-styled 48px "$0.00" under the label "Available earnings balance". Nothing
Fix. Destructure `isError` from `useProfileStats` in the wallet screen and, when `isError && !isFetching`, render `WalletLoadError` (title "Couldn't load your balance", onRetry `hardRefetchStats`) in place of `WalletHeroBalance`, and disable the Move/Withdraw actions. Give `WalletHeroBa
apps/mobile/app/(app)/(tabs)/wallet/index.tsx
2 37.4 Earnings query failure renders as the empty state
P1 Resilience effort S says something untrue Track referral earnings
The summary query uses retry:false with no error branch; on failure `data` is undefined and the screen falls into `if (!data || !groupedActivities.length)` showing 'No referral earnings yet… 🎉'. A user with real unclaimed money sees a celebratory zero and loses the Claim button.
Fix. Add an explicit error state ('Couldn't load your earnings' + Retry) distinct from the empty state; same fix applies to referral-activity.tsx which shares the pattern.
apps/mobile/app/(app)/(tabs)/referrals/rewards-screen.tsx
3 37.4 Referral Rewards screen shows "$0.00 Unclaimed" and the no-referrals empty state when the summary request fails
P1 Trust effort S says something untrue Track referral earnings
`referral.earnings.summary` is configured with `retry: false`, and the screen's only branch after loading is `if (!data || !groupedActivities.length)`. A network failure or a 5xx therefore falls into the same branch as "you have no referral rewards" and renders `ReferralsStats` with `data?.totalUnclaimed ?? "0.00"` — a
Fix. Destructure `isError` from the summary query and render an explicit error card with a Retry (mirroring the `isDashboardError && !dashboard` block in invites-screen.tsx) before the `!data || !groupedActivities.length` empty branch. Separate the two conditions: `!data` after an error is not th
apps/mobile/app/(app)/(tabs)/referrals/rewards-screen.tsx
4 36.4 Card balance renders "$0.00" on the wallet tab when the Rain balance query fails, with no error shown
P2 Trust effort S says something untrue Wallet home & activity
`useGetUserBalances` returns `balances: query.data ?? null`, collapsing "loading", "errored" and "genuinely zero" into one value. The wallet tab turns that into the literal string "$0.00" and paints it on the Visa card face, the card-detail face and the "How your money moves" explainer she
Fix. Destructure `error` from `useGetUserBalances` in wallet/index.tsx and, on error, replace the card balance with a dash/retry affordance (or reuse `useShowCardErrorOnChange` as rain/card.tsx does) rather than substituting "$0.00". Also distinguish the loading case with a skeleton.
apps/mobile/app/(app)/(tabs)/wallet/index.tsx
5 30.4 Progress query errors render as 'No missions'
P1 Resilience effort S says something untrue Progress (my missions)
ProgressView never reads isError from the three infinite queries; on a failed fetch isLoading is false and data is undefined, so ListEmptyComponent shows 'No active missions'. A user with a transient network error sees their in-flight missions 'gone', which at an earnings surface reads as lost work.
Fix. Consume isError/error per segment and render an error state with a Retry button (refetch) instead of the empty copy.
apps/mobile/src/modules/progress/progress-view.tsx
6 26.0 Activation celebration renders a blank black full-screen modal whenever the mission query isn't already resolved
P2 Resilience effort S Earn by completing missions
The route's only guard is `if (!mission) return null;`. There is no loading branch and no error branch. Because the layout registers this screen with `presentation: "fullScreenModal"`, `headerShown: false` and `contentStyle: { backgroundColor: "#000" }`, returning null does not fall back to anything — it produces a com
Fix. Render the celebration shell unconditionally and only gate the copy on the partner name. Concretely: seed `partnerName` from the mission-detail store / a passed route param so the screen can render immediately, and when `mission` is still undefined render `<ActivationSuccessScreen partnerName={fa
apps/mobile/app/(app)/missions/[identifier]/activation-success.tsx
7 23.7 Connect tab shows the "How it works" marketing page when the platform list fails to load, with no retry
P3 Trust effort S says something untrue Connect a platform (zkTLS verify → payout)
useConnectPlatforms never exposes an error state — it collapses a failed query to an empty array. The tab branches on length only, so a network failure and "there are genuinely no platforms" render identically: a full-screen explainer promising "Link the apps you already use like Spotify, Uber, and Netflix" and "y
Fix. Return `isError`/`refetch` from useConnectPlatforms and add a third branch before the length check: on error render a retry surface (the pattern already exists as WalletLoadError). At minimum, wrap HowConnectWorks' ScrollView in a RefreshControl wired to refetch so the state is recoverable.
apps/mobile/app/(app)/(tabs)/connect/index.tsx
8 20.3 Progress tab renders "No active missions" when the missions query fails, and the screen has no way to retry
P2 Trust effort S says something untrue Progress (my missions)
None of the three infinite queries expose or handle `isError`. On failure `data` is undefined, so `missions` is undefined, `filteredMissions` is undefined, the FlatList receives `[]`, and ListEmptyComponent prints a single grey line: "No active missions". A user who has real in-flight missions is told they have none. Worse, the
Fix. Destructure `isError`/`refetch` from the three useInfiniteQuery calls and render a distinct error state with a Try again button when the active tab's query errored; separately add a RefreshControl to the FlatList so any state is recoverable by pull-to-refresh.
apps/mobile/src/modules/progress/progress-view.tsx

+ 10 more in this pattern — see the Backlog tab.

02The app shows numbers and people it made up · 7 findings

Participant avatars on mission cards are generated, not real. The share card bakes in fabricated social proof. The invite screen multiplies a slider by a hardcoded $0.825 and headlines the result as “your potential monthly earnings”. None of it is sourced from anything.

Fabricated social proof is the fastest way to lose a user who checks. It is also the cheapest class to fix here — most of these are S-effort deletions, not features.
1 63.4 Fake participant avatars fabricate social proof
P1 Trust effort S says something untrue Earn by completing missions
Live feed cards fabricate social proof: mission-card-alt.tsx (lines 400-451, rendered at 277 via MissionFeed in explore-tab) shows 3 hardcoded i.pravatar.cc stock faces on every mission card, and share-mission-sheet.tsx (lines 192-227) does the same under an explicit "Social proof" comment with "{N} earning" text. The
Fix. Restore the commented-out real recentParticipants rendering with DefaultAvatar fallback, and either compute the earnings figure from actual payout data or drop the dollar amount.
apps/mobile/src/views/mission/components/overview/mission-participants.tsx
2 49.9 Fabricated social proof baked into the share card
P1 Trust effort S says something untrue Invite friends & share link
ShareMissionSheetContent renders three random pravatar.cc stranger faces and, when participants data is missing, a deterministic fake count of `10_000 + ((taskId * 2654435761) % 240_001)` displayed as 'N earning'. Users are handed a card with invented people and numbers to send to their friends — a brand/regulatory risk on a money app.
Fix. Drop the fabricated fallback (hide the social-proof row when real participant data is absent) and replace pravatar strangers with real anonymized avatars or none.
apps/mobile/src/components/share-mission-sheet.tsx
3 18.7 Referral "potential monthly earnings" is a fabricated per-person constant shown as a dollar headline
P2 Trust effort S says something untrue Track referral earnings
The invite screen's calculator multiplies a slider count by a hardcoded `perPersonEarnings = 0.825` and renders the result as a large-title dollar figure labelled "Your potential monthly earnings", under a heading that reads "See how much you can earn with referrals". At the top of the sliders it reaches $5,775.00/mont
Fix. Either derive the per-person figure from the user's own realised referral earnings (the dashboard already returns direct/indirect counts and amounts) or mark the output unambiguously as an illustration — smaller type, an explicit "Illustrative — actual earnings depend on what your friends do&qu
apps/mobile/src/components/referrals/earning-potential-calculator.tsx
4 17.0 App advertises a flat "5%" referral share; the payout engine pays 10% of a per-partner platform fee
P1 Trust effort M says something untrue Track referral earnings
The referrals screen states "Earn 5% when your friends earn - and 5% when their friends earn." That 5% is a hardcoded client string whose own comment admits it does not match the backend. The server computes a referral earning as `platformFee × 0.1`, where `platformFee = friendsRewardAmount × platformFeesPercentage` and `platfor
Fix. Stop stating a rate the payout engine does not use. Either (a) drive the label from the server's `program.directSharePercentLabel` and change the base wording from "when your friends earn" to what is actually paid ("a share of what ero earns from your friends' missions"), or (b)
apps/mobile/src/components/referrals/referral-share-display.ts
5 5.8 Claim celebration amount is rendered straight from route params, unverified
P3 Trust effort M says something untrue Wallet home & activity
The code-level claim is accurate: claim.tsx renders the celebration amount and fires reward_claimed analytics straight from an unvalidated route param (the tx hash is regex-checked, the amount is not), and an authenticated user can reach the screen with an arbitrary amount via a crafted earnos:/// deep link since +native-intent.tsx passes
Fix. Resolve the celebrated amount from the referralClaimTransactionId via tRPC (falling back to the param only while loading), so the on-screen figure always matches the ledger entry.
apps/mobile/app/(app)/claim.tsx
6 2.0 Orphaned verify.tsx KYC route in prod bundle
P3 IA effort S Identity verification (Sumsub)
app/(app)/verify.tsx is a genuinely orphaned route — zero navigation references in app/ or src/; wallet verification runs through the wallet-verify-sheet TrueSheet (wallet/index.tsx:347) — and it is deep-link addressable via path-style earnos:///verify (native-intent passes non-referral paths through) behind the (app) auth guard. But it i
Fix. Delete the route file or guard it with a redirect to /(app)/(tabs)/wallet.
apps/mobile/app/(app)/verify.tsx
7 1.7 Consent screen title hardcoded as “Create Virtual Card”
P3 Clarity effort S Get the Visa Spend Card
CreateCard defaults its title to “Create Virtual Card” (create-card.tsx:18) even when reused in contexts like re-accepting terms on an existing card.
Fix. Require an explicit title from each caller or derive it from card existence.
apps/mobile/src/modules/payout/components/create-card.tsx

03The same money means different things on different screens · 9 findings

“Available” on the wallet is the ledger balance; “Available” on cash-out is the on-chain balance — two numbers for the same money, one tap apart. Declined and reversed card transactions render a signed “−$120.00” as though money moved. Rejected referral earnings render “+$X” in earn-green. Payment and transfer rows show a green “Completed ✓” for statuses the mapper silently drops.

This app moves real money. A user who cannot reconcile two screens stops trusting both.
1 27.3 Payment and transfer transactions always show a green "Completed ✓" pill — including failed and pending ones
P1 Trust effort S says something untrue Use & manage the Spend Card
The backend returns a real status for `payment` (`status: z.string()`) and `transfer` (`status: z.enum(["pending","processing","settled","failed"])`) transactions, but `mapToRainTransaction` never copies it onto the mobile `RainTransaction`, leaving `status: undefined`. The receipt then calls `getRa
Fix. Carry the real status through the mapper for `payment` and `transfer` (they are the two variants that have one), widen `RainTransactionStatus` to cover the transfer enum (`processing`, `settled`, `failed`), and make `getRainTransactionStatusDisplay` return a neutral/grey unknown state for `undefined
apps/mobile/src/modules/payout/utils/rain-transaction-display.ts
2 25.4 Reserved payouts celebrated and shared as already earned
P2 Trust effort S says something untrue Connect a platform (zkTLS verify → payout)
mapProofSubmitResponseToPayout computes status 'reserved' when any line item is RESERVED, but ConnectRewardSuccessScreen never reads payout.status — the celebration headline and the share message ('I just earned $X for connecting…') treat reserved money as paid. A user checking their wallet immediately won't find the amount.
Fix. When payout.status === 'reserved', switch the celebration subcopy to 'reserved for you — pays out weekly' (and adjust the share string), so the money moment matches wallet reality.
apps/mobile/src/modules/connect/components/connect-reward-success-screen.tsx
3 14.6 A failed dispute fetch renders "Dispute Transaction" as if no dispute exists
P2 Trust effort S says something untrue Use & manage the Spend Card
`useRainDisputes` returns `error` and `isLoading`, but `RainReceiptContent` destructures only `{ disputes, refetch }`. Inside the hook the data is coalesced — `(query.data ??
Fix. Consume `isLoading`/`error` from `useRainDisputes` in `RainReceiptContent`: render a skeleton/disabled state for the dispute area while loading, and on error render an inline "Couldn't load dispute status — retry" affordance instead of the create-dispute button. Never present the create pa
apps/mobile/src/modules/payout/components/rain-transaction-receipt.tsx
4 11.8 Grid says 'Coming soon' while the sheet says Live + Connect
P3 Consistency effort S says something untrue Connect a platform (zkTLS verify → payout)
An 'available' platform with $0 estimatedEarning falls into the card's final else branch and renders a 'Coming soon' pill, but tapping it opens the gate with a green 'Live' pill and an enabled 'Connect {name}' primary — two contradictory statuses for the same platform one tap apart.
Fix. For available platforms without a reward, show a neutral 'Connect' pill on the card (reserve 'Coming soon'/'Upcoming' for status === 'upcoming' only).
apps/mobile/src/modules/connect/components/connect-earning-platform-card.tsx
5 10.9 A declined charge is headlined as "-$120.00", implying money left the account
P2 Clarity effort S says something untrue Use & manage the Spend Card
The hero amount is built purely from transaction type, never from status: `isOutbound` is true for every `spend`, so a declined authorization (no money moved) and a reversed one (money returned) both render as a 44px `-$120.00`. The only differentiators are an amber tint and the small status pill below it. On a money surface the dominant
Fix. For `declined`/`reversed`, drop the minus sign or strike through the hero amount and add a plain-language line ("This charge was declined — you weren't charged" / "This charge was reversed and refunded"). Align the amount colour with the pill colour so the two elements don't cont
apps/mobile/src/modules/payout/components/rain-transaction-receipt.tsx
6 10.9 Declined and reversed card transactions render a signed "-$X" as if money left the card
P2 Trust effort S says something untrue Use & manage the Spend Card
`CardTransactionRow` derives the sign solely from the transaction type (`spend`/`fee` → outbound) and never from status. A declined authorisation — where no money moved at all — and a reversed charge — where it moved and came back for a net zero — both render "-$42.00". The user reading the Spending feed to reconcile their card
Fix. Compute the prefix from status as well as direction: emit no sign (and use `text-subtlest`) when `status === "declined" || status === "reversed"`, matching `EarningsRow`'s `movedMoney` rule.
apps/mobile/src/modules/payout/components/card-transaction-row.tsx
7 7.8 Move screen validates against a hidden on-chain cap while displaying the higher ledger balance
P3 Clarity effort S Move money (Rewards ↔ Spend Card)
The Rewards Account row displays the ledger balance, but Max and the exceeds-check use `min(rewardsCents, walletCents)` where `walletCents` is the on-chain USDC balance. When on-chain is lower, the row says "$40.00", Max fills "$12.30", and typing $20 shows "Amount exceeds your available balance" directly abo
Fix. When `walletCents < rewardsCents`, show the binding figure on the Rewards row (e.g. "$40.00 · $12.30 available now") or set `balanceLabel` to the spendable cap with the ledger total as a secondary line. The user must be able to see the number the validator is using.
apps/mobile/app/(app)/(tabs)/wallet/move.tsx
8 7.6 "Available earnings balance" (wallet) and "Available $X" (cash out) are different numbers for the same money
P2 Trust effort M Cash out (Coinflow withdrawal)
The wallet tab's hero is the ledger-derived balance from `useProfileStats`. The Withdraw button sits directly under it and leads to the cash-out screen, whose "Available" figure and validation cap come from `useBaseUsdcBalance` — an on-chain USDC `balanceOf` read. These are two different quantities, and the codebase says so expl
Fix. Pick one authoritative number for the withdrawable amount and show it in both places, or label them distinctly ("Earned" vs "Ready to withdraw") and surface the settling portion on the wallet hero so the drop is explained rather than discovered at the cash-out screen. If the ledg
apps/mobile/app/(app)/payout/coinflow/cash-out.tsx

+ 1 more in this pattern — see the Backlog tab.

04The app promises specific things it does not do · 9 findings

“Notify me” registers no alert. Upcoming platforms are subtitled “Ready to verify”. The activation screen says rewards “land here automatically” when every milestone reward is claimed manually. The referrals screen advertises a flat 5% share while the payout engine pays 10% of a per-partner platform fee.

Each of these is one specific sentence a user will later discover was untrue. Cheap to fix, expensive to leave — and unlike a bug, the user experiences it as dishonesty rather than breakage.
1 23.0 'Notify me' promises a platform alert it never registers
P1 Trust effort M says something untrue Connect a platform (zkTLS verify → payout)
notifyMe() only requests push permission and fires a PostHog capture (connect_platform_notifications_enabled) — there is no tRPC mutation recording interest in that platform. The toast tells the user 'We'll let you know the moment this platform is ready', a promise the app has no backend subscription to keep.
Fix. Add a connect.platforms.notify (or interest-registration) mutation keyed by platformId, and only show the success toast after it succeeds; keep the analytics capture as a side signal.
apps/mobile/src/modules/connect/hooks/use-connect-platform-action.hook.ts
2 19.6 Locked "Move" and "Withdraw" pills are completely inert — tapping them does nothing and never explains why
P3 Feedback effort S Wallet home & activity
For every unverified (i.e. every brand-new) user, both primary wallet actions render with a lock glyph at 60% opacity and `disabled`, which means React Native never fires onPress. The wallet's own comment states the opposite intent — that a locked tap should route into verification — so the KYC funnel entry the code was designed around is
Fix. Drop `disabled` for the locked case and instead route the press through the existing gate (handleVerify("withdraw") / handleMoveMoney's KYC check), or at minimum show the reason on tap. Add `accessibilityRole="button"` and `accessibilityState={{ disabled: locked }}` with a label
apps/mobile/src/components/wallet/wallet-big-actions.tsx
3 13.1 Invited users get zero acknowledgment of the invite in the full app
P1 Trust effort M Invited user via referral link
capture-referral renders a bare dark View and redirects into the completely generic login/onboarding flow; a grep of src/ finds no 'invited by' copy anywhere in the RN app (only the App Clip has one). The invitee cannot tell the code was captured, gets no error if it wasn't, and there is no manual invite-code field on the login screen as
Fix. Show a lightweight 'You were invited by <name>' confirmation (the App Clip already fetches the referrer profile via a public endpoint) on the capture screen or as a banner on the login page, persisted through setup.
apps/mobile/app/capture-referral/[publicReferralCode].tsx
4 11.8 Upcoming platforms subtitled 'Ready to verify'
P3 Clarity effort S says something untrue Connect a platform (zkTLS verify → payout)
Factually accurate as written: getStatusConfig's "upcoming" case (connect-confirmation-sheet.tsx:262) sets platformSubtitle to "Ready to verify" — identical to the available-platform copy at line 283 — while the pill reads "Upcoming" and the only CTA is "Notify me". Additionally showQualifies: true
Fix. Change the upcoming subtitle to something like 'Not live yet — get notified' so pill, subtitle and CTA agree.
apps/mobile/src/modules/connect/components/connect-confirmation-sheet.tsx
5 11.6 Invitee-side referral capture is completely silent
P2 Feedback effort M Invitee opens referral link
capture-referral renders a bare background View, writes attribution to a store, and redirects; a grep for 'invited by' across app/ and src/ returns nothing, so the invited friend never sees who invited them, that the code was applied, or the 14-day mission deadline they're now on. Invalid/expired codes fail silently the same way. Progress
Fix. Show a brief interstitial or signup banner — inviter avatar/username, 'Your invite from @name is active — complete a mission within 14 days' — and persist it through the (public)/(setup) flow.
apps/mobile/app/capture-referral/[publicReferralCode].tsx
6 8.9 Waitlist gate is revealed only after username and photo are done
P2 Clarity effort M Account setup (username, photo, notifications)
The ONBOARDING_SIGNUP flag is known as soon as flags load, yet a waitlisted user still completes username and profile-picture before the notifications step reveals the wall; the server gate_blocked path can even reject completeOnboarding after everything, via a generic SERVER_ERROR sheet whose copy asks them to enable notifications withou
Fix. Check the waitlist flag at (setup) entry and route straight to the waitlist screen before asking for username/photo; give the gate_blocked sheet a real 'Notify me' CTA.
apps/mobile/app/(setup)/notifications.tsx
7 8.9 First-run education only exists behind an empty catalog or a tiny info icon
P2 Clarity effort M Connect a platform (zkTLS verify → payout)
The strong 'Get paid for apps you already use' education screen (HowConnectWorks) renders only when the catalog is empty — a state real users should never see — and the What-is-Connect sheet hides behind a 20px header info icon. A first-time visitor with a populated catalog gets dollar figures and brand cards with no explanation of why br
Fix. Show the info sheet (or a one-time inline education card above the grid) on first visit to the Connect tab, dismissible and persisted via storage flag.
apps/mobile/src/modules/connect/components/how-connect-works.tsx
8 5.7 No receipt or tracker handoff after 'Withdrawal submitted'
P2 Feedback effort M Cash out (Coinflow withdrawal)
On success the takeover shows "Withdrawal submitted" for 1.8s then router.replace()s to the wallet (confirmation.tsx:195-197), discarding the quote's fee breakdown, net "You receive" amount, and expected-delivery date with no receipt handoff. The discoverability half of the original claim is overstated: the earnings qu
Fix. After the success beat, land on (or deep-link a button to) the transaction receipt for the new withdrawal, persisting the expected-delivery estimate and net amount from the quote.
apps/mobile/app/(app)/payout/coinflow/confirmation.tsx

+ 1 more in this pattern — see the Backlog tab.

05The user acts, and nothing happens · 19 findings

Locked Move and Withdraw pills are inert taps with no explanation. A KYC rejection says “contact support” and offers no way to contact support. A completed withdrawal hands off to no receipt or tracker. An already-claimed mission silently dumps the user to the home feed. Invitee-side referral capture happens with no acknowledgement at all.

A dead end is worse than an error message. The user cannot tell whether the app is broken, whether they did something wrong, or whether it worked — so they stop trying.
1 39.0 Already-claimed result silently dumps user to home
P1 Feedback effort S Earn by completing missions
In MissionClaiming, when completeMission returns a resultKind other than 'completed' (already_completed / completed_between_checks), dismissAfterCelebration() fires with no message: the user taps 'Claim $X', watches the 'Processing reward' spinner, then lands on the home tab with no confirmation of whether they were paid. Progress (11 Jul
Fix. For already-completed results, show the celebration (the reward was credited) or an explicit 'Already claimed — check your wallet' state instead of a silent dismiss.
apps/mobile/src/views/mission/components/claiming/mission-claiming.tsx
2 25.2 Locked Move/Withdraw pills are dead taps for unverified users
P1 Clarity effort S Cash out (Coinflow withdrawal)
WalletBigActions sets disabled={action.locked} on the Pressable, so an unverified user tapping the primary Withdraw/Move pills gets nothing — no explanation, no route into verification. The wallet code comment even claims 'A locked tap routes into verification via handleCashOut's gate', but disabled prevents onPress from ever firing.
Fix. Keep the lock icon but let the tap fire: route locked taps to handleVerify("withdraw") (the intent-resume plumbing already exists), or show a one-line tooltip/sheet explaining the unlock path.
apps/mobile/src/components/wallet/wallet-big-actions.tsx
3 21.8 KYC-rejected state says 'contact support' but offers no support action
P1 Trust effort S says something untrue Identity verification (Sumsub)
The REJECTED banner in WalletVerifySheet reads 'Please contact support for help' but the sheet renders no support CTA (Intercom is wired elsewhere in the wallet), and the primary button below it stays enabled as 'Begin/Continue verification' — inviting a terminally rejected user to re-launch Sumsub, which will fail.
Fix. When kycStatus === "REJECTED", replace the launch button with a 'Contact support' button (Intercom.present(), matching the BLOCKED card stage) and suppress re-launch.
apps/mobile/src/components/wallet/wallet-verify-sheet.tsx
4 19.5 OTP resend failures are completely silent
P2 Feedback effort S Sign in with email OTP
handleResendOtp in otp-code.tsx rethrows any non-rate-limit error, and OtpResendButton's catch block only calls logger.error — no toast, no error state, and no cooldown starts. The user taps Resend, nothing visibly happens, and they wait for a code that was never sent.
Fix. Surface a toast/global error in the OtpResendButton catch (or in handleResendOtp) and keep the Resend link active so the user can retry knowingly.
apps/mobile/src/modules/auth/components/otp-resend-button.tsx
5 19.2 TikTok share opens TikTok with no link and nothing copied
P2 Feedback effort S Invite friends & share link
handleTikTok dismisses the sheet and calls `Linking.openURL("tiktok://")` — when TikTok is installed the user lands on the TikTok home feed with no referral link attached and nothing on the clipboard, so the share is silently lost; the native-share fallback only fires when TikTok is absent.
Fix. Copy the referral link to the clipboard before opening TikTok and toast 'Link copied — paste it in your post', or route TikTok through the native share sheet like Instagram.
apps/mobile/src/components/share-mission-sheet.tsx
6 14.4 Pull-to-refresh on the referral Rewards screen never shows a spinner and silently swallows a failed refresh
P2 Feedback effort S Track referral earnings
The RefreshControl's `refreshing` prop is bound to `isLoadingEarnings`, which is the query's isPending. Both places it is used sit *after* an early return that fires when isLoadingEarnings is true, so the prop is provably the constant `false` — the spinner snaps back the instant the finger lifts while `handleRefresh` runs a server-side re
Fix. Bind `refreshing` to the query's `isFetching` (or a local isRefreshing state set around handleRefresh), and wrap the refreshSummary/refetch pair in try/catch so a failed refresh still refetches and surfaces a message.
apps/mobile/app/(app)/(tabs)/referrals/rewards-screen.tsx
7 13.7 The celebration's close button is an unlabelled "✕" glyph — the only dismissal control for the first 4 seconds
P3 A11y effort S Earn by completing missions
CelebrationShell's close control is a bare `TouchableOpacity` wrapping a `Text` node containing the raw character "✕" (U+2715), with no `accessibilityRole`, no `accessibilityLabel` and no `accessibilityHint`. VoiceOver/TalkBack announce the glyph itself ("multiplication x" or nothing at all) rather than "Close&quo
Fix. Add `accessibilityRole="button"` and `accessibilityLabel="Close"` (and ideally `accessibilityHint="Returns to the mission"`) to the TouchableOpacity in CelebrationShell, and mark the ✕ Text `accessibilityElementsHidden` / `importantForAccessibility="no"` so th
apps/mobile/src/modules/reward/components/celebration-shell.tsx
8 13.4 "Share link" button looks fully enabled before the link resolves and does nothing when tapped
P3 Feedback effort S Invite friends & share link
The prominent white full-width `GlassButton` renders at full opacity from first frame, but `handleShareLink` starts with `if (!shareUrl) { return; }`. While the queries are in flight — and forever, if they fail — tapping the button produces no share sheet, no toast, no spinner, no disabled styling. It does fire haptic feedback (`GlassButt
Fix. Pass `disabled={!shareUrl}` (or `loading={!shareUrl}`) to the `GlassButton` so it dims and stops responding until the link resolves, matching the QR/Copy buttons in `ReferralLinkCard`.
apps/mobile/app/(app)/(tabs)/referrals/referral-qr.tsx

+ 11 more in this pattern — see the Backlog tab.

06The next release

Greedy fill of a 14-point effort budget from the top of the ranking — the same cut the Backlog tab will copy as a single bundled prompt.

“Stop lying, stop losing.”

12 items · 13.2/14 effort points · 11 of them S-effort
18.0Account deletion auto-executes on a timer — Profile & settings
13.1Sub-$1 earnings can expire while claim is locked — Track referral earnings
68.3Wallet hero shows "$0.00 Available earnings balance" when the stats fetch fails — Wallet home & activity
63.4Fake participant avatars fabricate social proof — Earn by completing missions
50.7Marketplace fetch failure masquerades as the education screen — Connect a platform (zkTLS verify → payout)
49.9Fabricated social proof baked into the share card — Invite friends & share link
39.0Already-claimed result silently dumps user to home — Earn by completing missions
37.4Earnings query failure renders as the empty state — Track referral earnings
37.4Referral Rewards screen shows "$0.00 Unclaimed" and the no-referrals empty state when the summary request fails — Track referral earnings
36.4Card balance renders "$0.00" on the wallet tab when the Rain balance query fails, with no error shown — Wallet home & activity
30.4Progress query errors render as 'No missions' — Progress (my missions)
27.3Payment and transfer transactions always show a green "Completed ✓" pill — including failed and pending ones — Use & manage the Spend Card

07Everything else

23 findings that do not belong to one of the patterns above — individually real, but they do not compound the way the themed ones do. Top 6 by rank.

1 50.7 Marketplace fetch failure masquerades as the education screen
P1 Resilience effort S says something untrue Connect a platform (zkTLS verify → payout)
ConnectTabIndex renders HowConnectWorks whenever platforms.length === 0 — which is also what a failed connect.platforms.list query produces (the hook exposes no error flag). HowConnectWorks is a static ScrollView with no RefreshControl and no CTA, so an offline or 500 user sees marketing copy with zero way to retry.
Fix. Surface query error from useConnectPlatforms and render a distinct error state with a Retry button; add pull-to-refresh to HowConnectWorks so a genuinely empty catalog can also recover.
apps/mobile/app/(app)/(tabs)/connect/index.tsx
2 26.0 Feed card icon actions have no accessibility labels
P2 A11y effort S Earn by completing missions
mission-card-alt.tsx — the live Explore feed card — contains zero accessibilityLabel/accessibilityRole props; the bookmark, share, and options icon actions (lines 286-340) are bare Pressables wrapping icons, so VoiceOver announces nameless, roleless elements and blind users cannot identify or use any of the three secondary feed actions. T
Fix. Add accessibilityRole="button" and labels ('Bookmark mission', 'Share mission', 'Mission options', 'Earn $X for {title}') to each Pressable, matching the pattern already used in HeaderIcon on the overview screen.
apps/mobile/src/components/mission-card-alt.tsx
3 25.0 QR sits on translucent glass, violating the component's solid-surface contract — the centre knockout renders as a mismatched dark patch
P2 Consistency effort S says something untrue Invite friends & share link
`ReferralQrCode` paints an opaque `#16171C` squircle over the centre of the code to carve the snowflake negative space, and its own docs state the container must paint that exact colour behind the code. This screen instead wraps it in `<GlassWrapper borderRadius={28}>` and passes no `surfaceColor`. On iOS 26 that is a translucent `G
Fix. Either paint the QR's own backing surface — wrap `ReferralQrCode` in a `View style={{ backgroundColor: QR_SURFACE_COLOR }}` with rounded corners as `ReferralQrSheetContent` does — or pass `surfaceColor` matching whatever the glass card actually resolves to. The first is safer since glass is dynamic.
apps/mobile/app/(app)/(tabs)/referrals/referral-qr.tsx
4 18.7 Rewards badge shows the wrong number and pushes can't reach Rewards
P2 Clarity effort S says something untrue Track referral earnings
The red badge on the 'Rewards' segment is bound to `friendsInvited` — a lifetime invite count that never clears — so it reads as permanent unread reward activity. Meanwhile the REFERRALS notification deep link (deep-link-registry.ts) lands on the tab whose store defaults to 'invites', so an earnings push cannot actually open the Rewards v
Fix. Badge on new/unclaimed earnings count and clear it when Rewards is viewed; support a segment param on the REFERRALS deep link that sets the store to 'rewards'.
apps/mobile/src/components/referrals/referrals-header.tsx
5 13.1 Rejected and reversed referral earnings render as "+$X" in earn-green
P3 Trust effort S says something untrue Track referral earnings
The referral activity row colours and signs the amount purely on `parseFloat(amount) > 0`, ignoring `status`. A REJECTED earning, and a clawed-back one (`CLAWBACK_READY`, captioned "Reversed"), both render "+$4.50" in the same earn-green used for money that actually arrived, with only a 13px grey/red caption underne
Fix. Mirror `EarningsRow`'s rule: compute `movedMoney = status === "CLAIMED"` (pending/unclaimed money has not landed; rejected/reversed never will), drop the `+` and the green for anything else, and use `text-subtlest` for non-moved rows so the caption and the amount agree.
apps/mobile/src/components/referrals/referral-activity-row.tsx
6 13.0 Filters close button labeled 'Leaderboard'
P2 A11y effort S Earn by completing missions
In the mission-filters sheet header, the xmark close button's native header item is declared with label: "Leaderboard" (copy-paste from another layout). On iOS 26 native header items this label is what assistive tech and long-press tooltips surface, so VoiceOver announces the Filters close control as 'Leaderboard'.
Fix. Change the label to "Close" in the platformHeaderItems left item of mission-filters/_layout.tsx.
apps/mobile/app/(app)/mission-filters/_layout.tsx
Generated from data/prod_atlas.json · 87 open of 89 findings
LoopScore = (reach × impact × sev × confidence × stage × lie) / effort · hazard overrides
Findings verified against origin/main 529eb97d5 · route ledger scanned at 200ccb80e on 2026-07-25
Regenerate: node scripts/build-report.mjs