diff --git a/apps/web/src/actions/rewards/claimRewardAction.ts b/apps/web/src/actions/rewards/claimRewardAction.ts
new file mode 100644
index 000000000..3e93061b4
--- /dev/null
+++ b/apps/web/src/actions/rewards/claimRewardAction.ts
@@ -0,0 +1,27 @@
+'use server'
+
+import { ClaimedReward, RewardType } from '@latitude-data/core/browser'
+import { claimReward } from '@latitude-data/core/services/claimedRewards/claim'
+import { z } from 'zod'
+
+import { authProcedure } from '../procedures'
+
+export const claimRewardAction = authProcedure
+ .createServerAction()
+ .input(
+ z.object({
+ type: z.enum(Object.values(RewardType) as [string, ...string[]]),
+ reference: z.string(),
+ }),
+ )
+ .handler(async ({ input, ctx }) => {
+ const workspace = ctx.workspace
+ const user = ctx.user
+ const result = await claimReward({
+ workspace,
+ user,
+ type: input.type as RewardType,
+ reference: input.reference,
+ })
+ return result.unwrap() as ClaimedReward
+ })
diff --git a/apps/web/src/actions/rewards/fetchClaimedRewardsAction.ts b/apps/web/src/actions/rewards/fetchClaimedRewardsAction.ts
new file mode 100644
index 000000000..c1600475c
--- /dev/null
+++ b/apps/web/src/actions/rewards/fetchClaimedRewardsAction.ts
@@ -0,0 +1,12 @@
+'use server'
+
+import { authProcedure } from '../procedures'
+import { ClaimedRewardsRepository } from '@latitude-data/core/repositories'
+
+export const fetchClaimedRewardsAction = authProcedure
+ .createServerAction()
+ .handler(async ({ ctx }) => {
+ const claimedRewardsScope = new ClaimedRewardsRepository(ctx.workspace.id)
+ const result = await claimedRewardsScope.findAllValidOptimistic()
+ return result.unwrap()
+ })
diff --git a/apps/web/src/components/layouts/AppLayout/Header/Rewards/Menu/index.tsx b/apps/web/src/components/layouts/AppLayout/Header/Rewards/Menu/index.tsx
new file mode 100644
index 000000000..400b027aa
--- /dev/null
+++ b/apps/web/src/components/layouts/AppLayout/Header/Rewards/Menu/index.tsx
@@ -0,0 +1,93 @@
+'use client'
+
+import { useMemo } from 'react'
+
+import {
+ ClaimedReward,
+ REWARD_VALUES,
+ RewardType,
+} from '@latitude-data/core/browser'
+import { Button, cn, Icon, Text } from '@latitude-data/web-ui'
+import useRewards from '$/stores/rewards'
+
+function RewardItem({
+ description,
+ type,
+ claimedRewards,
+ isLoading,
+}: {
+ description: string
+ type: RewardType
+ claimedRewards: ClaimedReward[]
+ isLoading: boolean
+}) {
+ const isClaimed = useMemo(() => {
+ if (isLoading || !claimedRewards) return false
+ return claimedRewards.some((r) => r.reward_type === type)
+ }, [isLoading, claimedRewards, type])
+
+ const runs = useMemo(() => `${REWARD_VALUES[type] / 1000}k`, [type])
+
+ return (
+
+ )
+}
+
+export function RewardsMenu() {
+ const { data: claimedRewards, isLoading } = useRewards()
+
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/layouts/AppLayout/Header/Rewards/index.tsx b/apps/web/src/components/layouts/AppLayout/Header/Rewards/index.tsx
new file mode 100644
index 000000000..947a37b77
--- /dev/null
+++ b/apps/web/src/components/layouts/AppLayout/Header/Rewards/index.tsx
@@ -0,0 +1,22 @@
+import { Button } from '@latitude-data/web-ui'
+import Popover from 'node_modules/@latitude-data/web-ui/src/ds/atoms/Popover'
+
+import { RewardsMenu } from './Menu'
+
+export function RewardsButton() {
+ return (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/web/src/components/layouts/AppLayout/Header/index.tsx b/apps/web/src/components/layouts/AppLayout/Header/index.tsx
index eb2310276..1342f0459 100644
--- a/apps/web/src/components/layouts/AppLayout/Header/index.tsx
+++ b/apps/web/src/components/layouts/AppLayout/Header/index.tsx
@@ -11,6 +11,7 @@ import { Fragment } from 'react/jsx-runtime'
import AvatarDropdown from './AvatarDropdown'
import { UsageIndicator } from './UsageIndicator'
+import { RewardsButton } from './Rewards'
function BreadcrumbSeparator() {
return (
@@ -112,6 +113,7 @@ export default function AppHeader({