Skip to content

Repository files navigation

My Activity — reward-clarity demo

CI Bugbot

A Next.js 15 reimagining of super.com/earn/activity. The live page tells you something is happening; this demo tells you what state every reward is in, when it was last checked, and what to do if it's stuck — using a closed four-status model (Tracking → In Progress → Rewarded → Needs Attention) on top of the same deals.getActivityOfferPlacements tRPC contract Super serves in production.

How closely this mirrors Super.com

This demo is deliberately built to match Super's real production stack — not just the UI, but the API contract, naming conventions, and architecture. Here's what's the same:

What Super This demo
UI library Material UI (MUI) v6 + Emotion MUI v6 + Emotion — same component primitives, same sx prop styling
Font Poppins (observed in HAR) Poppins via next/font/google — same weights, same scale
API style tRPC — observed at /snapcommerce/api/trpc/{router}.{procedure} tRPC v11, same path prefix, same snapcommerce namespace
Wire format snake_case field names — user_offer_activity, last_completed_at, icon_url, etc. Identical — Zod schemas and fixtures use the exact HAR field names
Response envelope { result: { data: { data: <payload> } } } doubly-nested superjson shape Identical — { data: ... } envelope preserved throughout
Procedure naming Verb-prefixed: getActivityOfferPlacements, updateActivityOfferPlacement Same convention for every procedure
Status model RETRY, IN_PROGRESS observed; larger enum in production deriveActivityStatus() maps the same values → four user-facing statuses; unknown enum values pass through
Analytics Amplitude Console-logged stub with Amplitude-shaped event names and properties
Auth stack Custom JWT; Ada chatbot JWT payload stub in the Ada dialog; chat UI matched to observed layout
Runtime Node.js on AWS Node.js via Vercel Fluid Compute — same runtime, different host

The main deviations are the framework (Next.js App Router vs. Super's webpack React SPA — the trade-off is collapsed frontend/backend/deploy at the cost of SSR semantics) and the host (Vercel vs. AWS — functionally identical for this surface). Everything user-observable — component library, font, API contract, field names — is matched field-for-field to what HAR captures show.


Live demo

The app is deployed at superalexyates.com. You don't need to run anything locally to explore it — just open the link below and every v2 feature is reachable from it:

superalexyates.com/earn/activity?scenario=mixed&admin=1

That combination loads one card per status and unlocks the admin tools for testing SSE, partner webhooks, and the Investigation Agent. See Admin flow below.

Want to run it locally instead? Follow the Quickstart below.


Quickstart

pnpm install
pnpm dev

Then open http://localhost:3000/earn/activity?scenario=mixed&admin=1.

Requirements: Node 20+, pnpm 10+ (via corepack enable).

Optional: set ANTHROPIC_API_KEY in a .env.local file to drive the Investigation Agent against live Claude. Without it, the agent falls back to a deterministic recorded response — useful offline and for CI.


Scenarios — same page, five fixture slices

The page reads ?scenario= and filters fixtures server-side in src/server/context.ts. No UI toggle — safe to leave wired in production.

URL What it shows
/earn/activity?scenario=mixed Hero demo — one card per status, in production order
/earn/activity?scenario=tracking Fresh pending — "Verifying…" copy, no CTA
/earn/activity?scenario=in-progress Multi-event offer with milestone progress + earnings line
/earn/activity?scenario=needs-attention RETRY + stale pending offers; best URL for the Investigation Agent
/earn/activity?scenario=rewarded Completed bucket — gold pill, muted surface

Append &admin=1 to any of them to unlock the admin tools.


Admin flow — how to test SSE, webhooks, and the agent

Add &admin=1 to any scenario URL. The flag is read client-side by useAdminMode and is gated against production by default. The canonical entry point:

superalexyates.com/earn/activity?scenario=mixed&admin=1

This URL is the fastest way to exercise every v2 pillar in under five minutes.

1. Watch SSE connect (30s)

  • Top of page, next to the title, look for the Live pill (ConnectionPill.tsx).
    • Green = SSE open. Amber = reconnecting. Gray = offline.
  • Open DevTools → Network → filter EventStream. You'll see one open connection to /api/events/activity with Content-Type: text/event-stream.
  • The very first frame is a snapshot event with latest_seq. Every subsequent frame is an activity event carrying one ActivityEvent.

2. Fire a partner webhook (60s)

Click the floating Partner Simulator button (bottom-right — only rendered when admin=1). Component: PartnerSimulator.tsx.

  1. Pick an offer (e.g. Uber Eats, which is Needs attention in the mixed scenario).
  2. Pick an event — attribution_verified, milestone_completed, attribution_failed, or reward_paid.
  3. Click Fire webhook.

What happens under the hood:

  • The simulator HMAC-signs the body with the demo secret and POSTs to /api/webhooks/partner.
  • The route verifies the signature via verifySignature, de-dupes on (partner_slug, idempotency_key) using idempotency.ts, appends an ActivityEvent to the in-memory log, and broadcasts it over SSE.
  • The card morphs in place: status pill animates, the card slides between sections via AnimatePresence over 240ms.
  • The connection pill pulses once on the incoming frame.
  • The simulator keeps a history of the last 10 fires (ok / duplicate / error).

Fire the same webhook twice — the second returns duplicate=true (200), not 202. Idempotency works.

3. Test multi-tab leader election (30s)

  1. Open a second tab to the same URL. Both tabs show Live.
  2. Open DevTools → Network in the second tab. You will not see an open EventStream.
  3. Fire a webhook from the simulator in tab one. Both tabs update simultaneously.

Why: useActivitySubscription runs a leader election over localStorage (leaderElection.ts, 3-second TTL) and a BroadcastChannel for fan-out. Only the leader holds the SSE; followers receive events over the channel. Close the leader tab and a follower auto-promotes.

4. Test SSE resume with Last-Event-ID (60s)

  1. With the Live pill green, stop the dev server (Ctrl+C).
  2. Wait 3–5s — pill goes amber.
  3. Restart (pnpm dev). The client reconnects, sends Last-Event-ID: <last seq> as a header, and the server replays events from getEventsSince(seq) in app/api/events/activity/route.ts.
  4. The pill goes green. No events missed.

This exact flow is a Playwright regression in playwright/my-activity-v2.spec.ts.

5. Run the Investigation Agent (2–3m)

Switch to ?scenario=needs-attention&admin=1 for stuck offers guaranteed to trigger a recommendation.

  1. On the Hulu+ Live TV card, click the small Why is this stuck? link.
  2. The right-side InvestigationDrawer opens and streams from /api/events/investigation/[offer_id].
  3. Watch the tool-call tiles animate: each tool_use renders with a spinner, then flips to a check mark when tool_result arrives.
    • queryOfferEvents — the offer's audit log
    • queryPartnerStatus — partner's attribution state (seeded to return ios_attribution_lag_24h)
    • queryUserOfferHistory — optional, only when signal is thin
  4. The answer streams in — 2-3 short sentences, per the system prompt.
  5. The footer shows a Recommended next step button. Click Re-check attribution.
  6. Drawer closes → webhook fires → card moves to the Continue earning section.

The round trip (recommendation → user click → real webhook → event in log → UI patch) is fully wired and is the most compelling beat in the demo.

Model: claude-haiku-4-5 with ephemeral prompt caching on the system prompt + tools (investigate.ts:182). Streaming via client.messages.stream. No extended thinking (Haiku 4.5 doesn't support it; answers stay tight regardless).

Demo mode fallback: If ANTHROPIC_API_KEY is unset, anthropic.ts returns null and runDemoInvestigation takes over — same shape (two tool calls + streamed answer), pre-canned iOS-lag answer, 12ms-per-char character streaming for feel. The drawer shows a Demo mode subtitle.

Caching: Completed investigations append an investigation_completed event. Reopening the drawer checks findCachedInvestigation at the current seq and hydrates instantly — no re-stream unless the event log has moved forward.

6. View the event log (30s)

Every mutation you just performed is in the log. Either:

  • Hit /api/trpc/deals.getActivityEvents?input=%7B%22since_seq%22%3A0%7D in a new tab, or
  • (If wired) open the LiveOpsConsole via Shift+Ctrl+L.

The UI is a pure projection of this log — projectOfferFromEvents reduces events into an OfferSnapshot. Restart the server and the log resets; that's the "three swaps to production" line in the demo script (swap #1 = move the log to Postgres).


What's in the demo

Four pillars (v2)

Pillar Primary files What to look for
Event-sourced ledger log.ts, projection.ts, types.ts Seven event kinds — offer_created, offer_updated, offer_deleted, activity_status_changed, partner_webhook_received, event_milestone_completed, investigation_completed. Every mutation appends; the read path is a deterministic projection.
SSE broadcast + multi-tab sync route.ts, useActivitySubscription.ts, leaderElection.ts, applyEvent.ts One EventSource per browser, leader-elected via localStorage + BroadcastChannel, auto-reconnect with Last-Event-ID replay, 30s heartbeat, exponential backoff [1, 2, 5, 10, 10]s.
Partner webhook simulator route.ts, hmac.ts, idempotency.ts, translate.ts, PartnerSimulator.tsx HMAC-SHA256 signed, idempotent on (partner_slug, idempotency_key), returns 202 on new / 200 on duplicate. Admin panel gated behind ?admin=1.
Investigation Agent investigate.ts, tools.ts, system-prompt.ts, cache.ts, InvestigationDrawer.tsx claude-haiku-4-5 streaming with tool use (3 tools), prompt caching, and a demo-mode fallback. Results cached against the event log seq so re-opening is instant.

Status model & cards

  • Four user-facing statuses — derived in deriveActivityStatus.ts from bucket + user_offer_activity.status + created_at. Unknown enum values pass through, never throw.
  • Four card variantsTracking, InProgress, Rewarded, NeedsAttention — dispatched by OfferActivityCard.tsx.
  • Card morph animationAnimatePresence + motion.div from the motion package. When status changes, old card fades out -6px, new card fades in +6px over 240ms (motion/constants.ts).
  • Status palette — all colors live in src/theme/muiTheme.ts under theme.palette.activity.{tracking,in_progress,rewarded,needs_attention}. Zero hex strings outside the theme.

Other polish

  • Glass-morphism card surface — frosted gradients in GlassCard.tsx, backed by theme.palette.glass.*.
  • Animated earnings count-upAnimatedNumber.tsx in the page header.
  • Progress affordancesProgressRing (SVG circle for % complete) and MilestoneProgress (checklist for multi-event offers).
  • RelativeTimestamp (RelativeTimestamp.tsx) using Intl.RelativeTimeFormat — "5 min ago", auto-updating.
  • Manual refresh — header button with 10s client-side throttle (useRefreshActivity.ts) and inline "Checking for updates…" state.
  • Ada support stubAdaSupportMock.tsx opens a labeled Dialog with the would-be JWT payload, clearly marked (demo stub).
  • Coming-soon modalComingSoonModal.tsx for CTAs that would route to a real partner page.
  • Needs-attention bannerNeedsAttentionStrip.tsx at the top when any offers are stuck.
  • Conflict toastConflictToast.tsx for concurrent-mutation scenarios.
  • Analytics stub — console-logged, Amplitude-shaped events in src/lib/analytics.ts. 14 events covering views, clicks, webhook fires, and investigation actions.
  • Web Vitals telemetrysrc/lib/telemetry/ (LCP, CLS, INP, TTFB, FCP).

tRPC surface

All procedures live in src/server/routers/deals.ts under deals.*. Paths are served at /api/trpc/deals.{procedure}.

Procedure Kind Purpose
getActivityOfferPlacements query Projected offers by bucket (pending / in_progress / completed), filtered by ?scenario=. Matches the HAR shape field-for-field (snake_case, data.data envelope).
getActivityEvents query Paginated audit log — returns events where seq > since_seq plus latest_seq.
createActivityOfferPlacement mutation Seed a new offer; appends offer_created.
updateActivityOfferPlacement mutation Patch an offer; appends offer_updated.
deleteActivityOfferPlacement mutation Soft-delete; appends offer_deleted.
getInvestigation query Cache check — returns a cached investigation for the current seq, or null.
simulatePartnerWebhook mutation Fire a fake partner webhook — same code path as the real route, used by tests and the admin panel.

Conventions (locked in CLAUDE.md): verb-prefixed procedure names, snake_case on the wire, TRPCError with proper codes (NOT_FOUND, BAD_REQUEST, UNAUTHORIZED), { data: ... } response envelope.


File layout

app/
├── earn/activity/page.tsx             # Server Component, SSR-prefetch + HydrationBoundary
├── layout.tsx                         # Theme, Poppins font, tRPC + emotion providers
└── api/
    ├── trpc/[trpc]/route.ts           # tRPC fetch adapter
    ├── events/
    │   ├── activity/route.ts          # SSE: snapshot frame, Last-Event-ID replay, 30s heartbeat
    │   └── investigation/[offer_id]/route.ts  # SSE: streams Investigation Agent events
    └── webhooks/
        └── partner/route.ts           # HMAC-verified, idempotent webhook receiver

src/
├── server/
│   ├── trpc.ts                        # initTRPC + base procedure
│   ├── context.ts                     # reads ?scenario=, exports SCENARIOS union
│   ├── routers/
│   │   ├── _app.ts                    # appRouter = { deals: dealsRouter }
│   │   └── deals.ts                   # Every tRPC procedure + tests alongside
│   ├── events/
│   │   ├── log.ts                     # appendEvent, getEventsSince, onAppend listener registry
│   │   ├── projection.ts              # events → OfferSnapshot
│   │   ├── broadcaster.ts             # SSE fan-out from log listeners
│   │   ├── buildOffer.ts              # snapshot → wire envelope
│   │   └── types.ts                   # ActivityEvent union, OfferSnapshot, Bucket, ScenarioKey
│   ├── ai/
│   │   ├── anthropic.ts               # client singleton, isDemoMode()
│   │   ├── investigate.ts             # streamInvestigation async generator (live + demo)
│   │   ├── tools.ts                   # 3 tool definitions + runTool(name, input)
│   │   ├── system-prompt.ts           # INVESTIGATION_MODEL = 'claude-haiku-4-5'
│   │   └── cache.ts                   # findCachedInvestigation(offer_id, seq)
│   ├── webhooks/
│   │   ├── hmac.ts                    # signBody, verifySignature (constant-time)
│   │   ├── idempotency.ts             # composite key store
│   │   ├── translate.ts               # partner event → ActivityEvent kind
│   │   └── process.ts                 # end-to-end webhook → event pipeline
│   └── fixtures/
│       └── scenarios.json             # One fixture, 4 keys (tracking, in_progress, rewarded, needs_attention)
├── features/
│   └── earn/activity/
│       ├── components/                # Cards, drawer, hero, pills, tiles, progress, modals
│       ├── hooks/                     # useActivityOfferPlacements, useInvestigation, useRefreshActivity
│       ├── admin/                     # PartnerSimulator, useAdminMode, signPartnerWebhook
│       └── lib/                       # deriveActivityStatus, deriveActivityTotals, formatRelative, types
├── lib/
│   ├── analytics.ts                   # console.log stub, Amplitude-shaped events
│   ├── motion/constants.ts            # CARD_MORPH_MS = 240
│   ├── realtime/
│   │   ├── useActivitySubscription.ts # Main SSE hook (leader-elected)
│   │   ├── leaderElection.ts          # localStorage lease with 3s TTL
│   │   ├── applyEvent.ts              # ActivityEvent → React Query cache patch
│   │   ├── useApplyServerEvents.ts    # Apply a batch from a webhook response
│   │   ├── useEventPolling.ts         # Polling fallback (reserved)
│   │   ├── optimistic.ts              # Optimistic store (reserved for v3)
│   │   └── useOptimisticStore.ts
│   ├── telemetry/                     # Web Vitals reporting
│   ├── trpc.ts                        # Client setup
│   └── trpcProvider.tsx               # Provider; forwards ?scenario= through links
└── theme/
    └── muiTheme.ts                    # palette.activity.*, palette.glass.*, Poppins typography

playwright/
├── my-activity.spec.ts                # v1 smoke: page load, card states, filtering
├── my-activity-v2.spec.ts             # SSE connect, webhook fire, multi-tab sync, Last-Event-ID resume
└── my-activity-v2-optimistic.spec.ts  # Reserved

.github/workflows/ci.yml               # typecheck → test → build → playwright (ANTHROPIC_API_KEY forced empty)

Environment variables

Variable Required Purpose
ANTHROPIC_API_KEY No Enables live Investigation Agent. If unset or empty, the agent falls back to demo mode. CI forces this to '' for deterministic runs.
PARTNER_HMAC_SECRET No HMAC secret for webhook verification. Defaults to demo-partner-secret in hmac.ts. The admin simulator signs with the same default, so webhooks work out of the box.

Scripts & quality gates

pnpm dev                       # localhost:3000
pnpm typecheck                 # tsc --noEmit
pnpm test                      # vitest run
pnpm test:watch                # vitest in watch mode
pnpm e2e                       # playwright test
pnpm build                     # next build
pnpm start                     # next start (requires prior build)
pnpm lint                      # next lint

CI

.github/workflows/ci.yml runs typecheck → test → build → Playwright on every push and pull_request. ANTHROPIC_API_KEY is forced empty so the Investigation Agent runs in demo mode on CI (deterministic, fast). Live Anthropic calls are exercised via mocked unit tests in src/server/ai/investigate.test.ts. Traces upload on failure.

Test coverage highlights


Stack

Layer Choice Mirrors Super?
Framework Next.js 15 App Router + TypeScript (strict) Demo deviation (Super uses webpack SPA)
API tRPC v11 + @tanstack/react-query v5 Yes
UI MUI v6 + Emotion, Poppins via next/font/google Yes
Motion motion v12
AI @anthropic-ai/sdk + SSE
Validation Zod Likely
Tests Vitest + Testing Library, Playwright
Package manager pnpm 10
Deploy Vercel (Fluid Compute defaults) Demo deviation (Super on AWS)

Full rationale and locks in CLAUDE.md §3 and docs/super-architecture.md.


Bugbot is also wired into CI and automatically reviews PRs for regressions and bugs.

Deploy (Vercel)

Stock Next.js 15 project — vercel --prod ships it as-is.

pnpm dlx vercel login
pnpm dlx vercel link
pnpm dlx vercel --prod

Set ANTHROPIC_API_KEY in project env for live agent. Smoke-test by opening each scenario URL (?scenario=mixed&admin=1, ?scenario=needs-attention&admin=1) on the production hostname in an incognito window — SSE, webhooks, and the agent should all work.


Demo script

docs/demo-script.md — seven-minute walkthrough you can run cold against the preview URL. Six acts, each ~60–180s:

  1. Baseline (?scenario=mixed) — four sections, Live pill, Refresh
  2. Partner push (?scenario=mixed&admin=1) — fire attribution_verified, watch card morph
  3. Multi-tab sync — same URL in two tabs, verify BroadcastChannel fan-out
  4. Investigation Agent (?scenario=needs-attention&admin=1) — streams, tools, recommendation
  5. Under the hood — DevTools EventStream, getActivityEvents audit log
  6. What would change in production — three swaps (event log, secrets, real tools)

If you only have 90 seconds: run Act 4 alone. It carries the demo.


Docs

About

Super My Activity improvements demo by Alex Yates

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages