Companion to:
- my-activity-implementation-phases.md — v1 (shipped)
- my-activity-page-progress-improvement-plan.md — original product brief
Primary target: Turn the shipped v1 demo into a small-but-real rewards platform — event-sourced state, partner webhooks, SSE fan-out, and a Claude-powered investigation agent that can answer "why is this offer stuck?" with streamed reasoning and tool calls. One 12-hour grind. Same deploy URL.
Stretch: Optimistic mutations + conflict reconciliation, a live-ops console with p50/p95 + Web Vitals, and a time-travel scrubber that replays the event log.
What I am NOT doing: re-theming, re-scoping the four-status model, rewriting v1 cards, or leaving the tRPC + snake_case contract. v1 is load-bearing — v2 sits on top of it.
- ~12 hours, solo. This plan gets there with a ~45 min buffer.
- v1 is already shipped and deployed. v2 is additive — nothing in v1 is rewritten.
- Every phase ships a demoable moment. Even if the clock bites at phase 5, the build is still ahead of any "polish the existing demo" plan.
- One PR per pillar, four PRs total: (1) event log + SSE, (2) webhook simulator + motion, (3) Investigation Agent, (4) CI + polish + demo.
- This plan is the one I'd show the hiring panel. It's not trying to be exhaustive — it's trying to be the most Super-shaped thing I could build in a day.
The v1 demo is a static snapshot of a rewards UX. Every additive-scope item in the v1 plan is real but incremental: one more test, one more token, one more fixture file. None of them change the shape of the demo.
A rewards platform has four real-world properties that the v1 demo only hints at. Ship all four and the demo stops being a mock and starts reading like a reference architecture:
| Pillar | Real-world property it mimics | What the v1 demo does today |
|---|---|---|
| Event-sourced ledger | Every offer state change (from partner callback, user action, ops override) is an auditable event. Projections are rebuildable. | Fixture resets on cold start; mutations overwrite state in place. |
| Partner webhook → SSE fan-out | Attribution confirmations land as partner webhooks; users see cards update without reloading. | Client-triggered refetch only; no push channel. |
| AI Investigation Agent | Ops, support, and users all want "why is this stuck?" answered in natural language. Future of rewards support. | Static copy on Needs Attention cards. |
| Live-ops console | A rewards team lives in a dashboard: latency, error rates, in-flight events. | No observability surface at all. |
Each pillar is individually impressive. Together they form the exact shape of the backend Super actually runs — attribution, fraud, support, ops. Finishing all four in a day is the signal.
Additions to the v1 stack in CLAUDE.md §3. Everything else stays put.
| Addition | Choice | Why |
|---|---|---|
| Real-time transport | Server-Sent Events over a Next.js App Router route handler | One-way server→client push; works cleanly on Vercel Functions (Fluid Compute). No socket infra, no protocol dance. Already called out as the v2 path in CLAUDE.md §4.5. |
| Pub/sub (server) | In-process EventTarget or mitt-style emitter |
Lives next to the in-memory store. No external broker. Mirrors the demo's intentional single-process boundary. |
| Tab coordination (client) | BroadcastChannel |
Multi-tab sync from a single SSE connection. One tab is the leader, others subscribe. |
| LLM | @anthropic-ai/sdk with prompt caching + extended thinking + tool use + streaming |
The Investigation Agent is the centerpiece. Anthropic SDK is production-grade, supports every primitive I want to demo. |
| Model | claude-sonnet-4-6 (streaming + extended thinking) |
Supports extended thinking and streaming; ~5x cheaper than Opus 4.7, sufficient for this use case. Upgrade to claude-opus-4-7 locally for maximum reasoning quality — both work identically in the UI. |
| Motion | Framer Motion (motion v11) |
Variants on state change sell "this just happened" in ≤300ms. Nothing else in the repo needs this. |
| Metrics | web-vitals (v4) + an in-memory latency histogram |
Client-side LCP/INP/CLS piped to the console panel; server-side timing via performance.now() around every procedure. No vendor beacon — honest and self-contained. |
| CI | GitHub Actions: typecheck → test → build → playwright |
Gate every PR. Green CI badge is part of the demo. |
| Env | ANTHROPIC_API_KEY required for Investigation Agent; demo fallback mode if absent |
Fail closed, not open. The fallback is a pre-recorded streaming response so the demo never dies. |
Explicitly not adding:
- WebSockets (SSE is the right primitive for one-way push; WS adds protocol cost we don't need).
- A database (the in-memory event log is deliberate, per CLAUDE.md §4.6).
- A real queue (Vercel Queues is the right production answer; scripted HTTP is fine for a 12-hour demo).
- State management libraries (React Query +
EventTargetis all the state we need).
Every one of these is defensible work. None of them land the "holy shit" moment in 12 hours.
| Cut | Why it's safe |
|---|---|
| Real Amplitude wire-up + flag gating | The console-log stub already carries the right event shape. Replacing it buys zero demo minutes. |
| Storybook + per-variant stories | Scenario switcher on the real page is still a better review tool for a single engineer. |
Design token export + /theme-preview route |
MUI theme is the spec. One consumer; no second caller. |
Separate tRPC routers for rewardsArbiter.* / credits.* / user.* |
The demo is on deals.*. Every other router observed in the HAR is scope creep. |
| Full-fidelity auth (JWT, cookies, Ada JWT mint) | Out of scope by design (CLAUDE.md §5). Public procedure still fine. |
| Real Neon / Upstash integration | Adds infra setup, loses the "one pnpm install, everything works" property. Call it out in the README as the deliberate next step. |
| Dark mode | Zero demo value in a daylight pitch; 90 min of fiddling. |
| i18n / localized relative timestamps | en-US is enough; noted as additive scope. |
Ten phases. Seven are must-ship (the core pillars). Three are stretch (drop in order if the clock bites). Every phase names what "done" looks like so you can cut early without drift.
Must-ship: P1 → P2 → P3 → P4 → P5 → P6 → P7 (≈9h, with 45min buffer)
Stretch: P8 → P9 → P10 (+≈3h if pace holds)
Why it's first. Everything else (SSE, webhooks, the AI agent, time travel) reads the event log. Ship the spine before the limbs.
Goal. Every state change — create, update, delete, webhook-driven re-verification, ops override — is an append-only ActivityEvent. The current deals.getActivityOfferPlacements response becomes a projection of the event log. Existing v1 tests still pass.
Deliverables:
src/server/events/types.ts— discriminated unionActivityEvent:type ActivityEvent = | { kind: 'offer_created'; at: string; actor: Actor; offer_id: string; payload: OfferSnapshot } | { kind: 'offer_updated'; at: string; actor: Actor; offer_id: string; patch: OfferPatch } | { kind: 'offer_deleted'; at: string; actor: Actor; offer_id: string } | { kind: 'activity_status_changed'; at: string; actor: Actor; offer_id: string; from: string | null; to: string | null; reason: string | null } | { kind: 'partner_webhook_received'; at: string; actor: Actor; offer_id: string; event_name: string; idempotency_key: string } | { kind: 'event_milestone_completed'; at: string; actor: Actor; offer_id: string; event_id: string; payout: string | null }; type Actor = { kind: 'user'; id: string } | { kind: 'partner'; slug: string } | { kind: 'ops'; id: string } | { kind: 'system' };
src/server/events/log.ts— in-memory append-only array keyed byoffer_id, plus a monotonically increasingseq. Two exports:appendEvent,getEventsSince(seq). Both are pure functions of the module-level ref.__resetActivityEventLog()for tests.src/server/events/projection.ts—projectOfferFromEvents(events): OfferSnapshot | null. Deterministic fold over the event stream; returnsnullif the offer was deleted. Covered by 6–8 Vitest cases.deals.tsrefactor — every mutation writes an event, then rebuilds the projection; read path still returns the same envelope.__resetActivityStore()seeds the event log from fixtures rather than cloning a record. Existingdeals.test.tsstays green without change.- New query:
deals.getActivityEventswith{ since_seq: number }— returns{ events: ActivityEvent[]; latest_seq: number }. Used by time-travel (Phase 10) and the Investigation Agent (Phase 5).
Skip: persistence, snapshots every-N-events, CDC-style compaction, Kafka semantics. In-memory and deterministic is the point.
Done when:
pnpm testgreen; all v1 tests untouched.deals.getActivityEventsreturns a seeded event per fixture offer (anoffer_createdevent per scenario entry).- Creating an offer via
createActivityOfferPlacementappends oneoffer_createdevent; updating appends oneoffer_updated; deleting appends oneoffer_deleted. Read path is unchanged.
Why it's second. The event log is useless until the client can subscribe to it. Ship the transport now so every subsequent phase can push events for free.
Goal. The browser subscribes once to /api/events/activity, receives ActivityEvents in real time, and the deals.getActivityOfferPlacements React Query cache updates without a refetch. Multi-tab sync works via BroadcastChannel. Connection is self-healing.
Deliverables:
src/server/events/broadcaster.ts— tinyEventTarget-backed pub/sub:broadcast(event),subscribe(handler): unsubscribe. WrapappendEventso every mutation fans out automatically.app/api/events/activity/route.ts— SSE endpoint:Content-Type: text/event-stream,Cache-Control: no-cache, no-transform,Connection: keep-alive,X-Accel-Buffering: no.- Sends a
retry: 3000directive + an initialevent: snapshotwithlatest_seq. - Respects
Last-Event-IDheader — replays missed events from the log on reconnect. - 30-second heartbeat (comment
:hbframes) so proxies don't kill the connection. - Cleans up subscribers on
request.signal.aborted(Fluid Compute graceful shutdown).
src/lib/realtime/useActivitySubscription.ts— React hook:- Opens an
EventSourcelazily on mount; closes on unmount. - Exponential backoff on
onerror(1s → 2s → 5s → 10s, capped). - Elects a leader tab via
BroadcastChannel('activity')+localStoragelock. Only the leader holds the SSE; followers receive events over the channel. - Exposes
{ status: 'connecting' | 'open' | 'reconnecting' | 'offline', lastEventAt, eventsReceived }.
- Opens an
src/lib/realtime/applyEvent.ts— small reducer that takes anActivityEventand mutates the React Query cache viaqueryClient.setQueryData(['deals.getActivityOfferPlacements', ...], updater). No refetch, no round trip.MyActivityPagewires the hook. Header gains a subtle connection pill — green dot + "Live" when open, amber + "Reconnecting…" on backoff, grey + "Offline — last update 12s ago" when the browser reports!navigator.onLine. No fake "Live" claims: the pill's text is always literal (§4.5 of CLAUDE.md).
Skip: actual distributed leader election, a serialized resume-token protocol beyond Last-Event-ID, multi-region fan-out. Single process is the contract.
Done when:
- Open two tabs on
?scenario=mixed. CallcreateActivityOfferPlacementvia a curl in a third terminal. Both tabs show the new card within 1s, without a network refetch. - Kill the dev server, wait 3s, restart. The pill goes amber then green; a
createActivityOfferPlacementfired while down arrives after reconnect viaLast-Event-IDreplay. - Only one tab holds an open
EventSourceat a time — verifiable via DevTools Network.
Why it's third. SSE is push without a reason to push. Webhooks are the reason. This phase turns the demo into a two-actor system: "something happened at the partner → the user sees it."
Goal. A partner sends a webhook → the server verifies + applies it → the event log grows → SSE fans out → cards animate to their new state. A demo-only admin panel lets you trigger any of the canonical partner events on any offer, live.
Deliverables:
app/api/webhooks/partner/route.ts— POST endpoint accepting:- Signature verification: HMAC-SHA256, constant-time compare. Secret from
PARTNER_HMAC_SECRETenv var (a demo default is checked in). - Idempotency: dedupes on
(partner_slug, idempotency_key), scoped in-memory. Duplicate POST is a 200 no-op, never a 500. - Returns 202 with
{ event_seq: number }so the partner could reconcile.
- Signature verification: HMAC-SHA256, constant-time compare. Secret from
src/server/webhooks/translate.ts— maps partner events toActivityEvents:attribution_verified→activity_status_changedfromRETRYorINITIATEDtoIN_PROGRESSmilestone_completed→event_milestone_completed+ a derivedoffer_updatedpatch marking the event completeattribution_failed→activity_status_changedtoRETRYreward_paid→activity_status_changedtoCOMPLETED
src/features/earn/activity/admin/PartnerSimulator.tsx— demo-only floating control (gated behind?admin=1):- Pick an offer from the current scenario.
- Pick a partner event (dropdown).
- "Fire" button posts to
/api/webhooks/partnerwith a correct signature. - History log of fired events in-panel, so you can demo a sequence.
- tRPC:
deals.simulatePartnerWebhook(mutation) for Playwright to drive without posting raw HTTP.
Skip: persistent idempotency store, real HMAC secret rotation, replay-attack prevention beyond idempotency, a per-partner allow-list. Scripted demo, not pentest.
Done when:
?scenario=needs-attention&admin=1→ fireattribution_verifiedon a RETRY offer → the card morphs from Needs Attention to In Progress within 500ms, progress bar animates in.- Firing the same
idempotency_keytwice emits exactly one event in the log. - Invalid signature → 401, no event written.
Why it's fourth. SSE + webhooks already update state. Motion turns state change into perceived causality — the user sees something happen. No motion and the cards feel like they're merely refreshing.
Goal. Status transitions animate. Progress bars tween. "Last updated" ticks visibly. The Live pill pulses briefly on each received event. Nothing janky, nothing distracting.
Deliverables:
motionv11 installed. OneAnimatePresenceat theOfferActivityCardroot;layouton the status pill to auto-morph between variants.StatusPillbecomes amotion.spanwith spring transitions on color + text.MilestoneProgress—LinearProgresswrapped with a tween from oldpercentto new on data change. 400ms ease-out.RelativeTimestamp— a single shared 1s tick (Phase 2 already centralizes the refresh interval); text fades in/out on cross-bracket changes ("Just now" → "1 min ago").- Header
connection pill— on each received event, a subtle background pulse (150ms, viacontrols.start+ reset). Never more than 4 Hz even under load. prefers-reduced-motionrespected globally — all transitions become instant.
Skip: card flip animations, entry stagger, confetti on reward_paid. Tasteful > flashy.
Done when:
- Firing
attribution_verifiedon a Needs Attention card: the pill morphs amber → green, the body content cross-fades, the progress bar tweens from 0 to 10% — all within 500ms, smooth on 60Hz. prefers-reduced-motion: reducein DevTools → every transition collapses to instant.
Why it's fifth. This is the centerpiece. Everything before this phase exists so this phase has something real to investigate. Every rewards/fintech platform is going to build this in the next 18 months; ship it in 180 minutes.
Goal. Clicking "Why is this stuck?" on a Needs Attention card opens a drawer. An agent powered by claude-sonnet-4-6 streams its reasoning (extended thinking), calls tools to query the event log, the partner attribution status, and a user history, and produces a human-readable explanation plus a recommended next step. The final answer is stored against the offer's event log as an investigation_completed event, so the drawer is re-openable without re-running the model.
Deliverables:
Server:
src/server/ai/anthropic.ts— lazy singleton Anthropic client. ReadsANTHROPIC_API_KEY; in its absence, switches to ademoModeclient that streams a pre-recorded response (stored as JSONL). The demo script uses real mode; the fallback means the preview URL never errors even if the key expires.src/server/ai/tools.ts— three tool definitions:Each tool has a Zod input schema + a pure server function. The partner and user history are scripted — they return deterministic synthetic data from the fixture so the demo is repeatable.queryOfferEvents({ offer_id, since_iso? }) → ActivityEvent[] queryPartnerStatus({ partner_slug, offer_id }) → { attribution_last_checked, last_known_state, known_issues[] } queryUserOfferHistory({ user_id, partner_slug }) → { total_offers, completion_rate, typical_time_to_verify_minutes }
src/server/ai/system-prompt.ts— system prompt with prompt caching breakpoint (stable block). Sets role, tone (calm, specific, no apologies), output contract (markdown with a final## Recommended next stepsection).- New tRPC procedure
deals.investigateOffer— usestrpc-subscriptions-style or a custom SSE route under the same endpoint (/api/events/investigation/[offer_id]). Streams three event kinds to the client:thinking— extended-thinking deltas (stream.on('thinking', ...))tool_use— the name + args of each tool call, then the resulttext— final answer deltasdone— finalinvestigationrecord (stored asinvestigation_completedin the event log)
- Server uses
max_tokens: 1024,thinking: { type: 'enabled', budget_tokens: 2000 }, prompt caching on the system prompt + tool definitions, streaming = true. Covers all four headline features of the Claude API at once.
Client:
src/features/earn/activity/components/InvestigationDrawer.tsx— MUIDraweranchored right, 520px wide:- Header: offer icon, title, status pill.
- Reasoning panel (collapsed by default, expands while thinking streams): scrollable monospace list of tool calls + thinking tokens. Every tool call renders as a card:
queryOfferEvents(offer_id="…", since_iso="…")→ JSON result pretty-printed. Thinking blocks appear as italic grey text. - Answer panel: rendered markdown, streaming in.
- Footer: "Recommended next step" pull-quote card with an action button (e.g. Re-check attribution → fires a partner
attribution_verifiedwebhook for the demo).
NeedsAttentionCardgains a secondary linkWhy is this stuck?(primary staysGet support). Clicking opens the drawer.- Drawer streaming UI uses Framer Motion
AnimatePresenceon the message list; new tool calls slide in. - Investigation results are memoized per
offer_id + latest_seq— re-opening the drawer on unchanged state rehydrates from the event log; changed state prompts a "Re-investigate" button.
Demo seams:
- The three tools are scripted. For the demo,
queryPartnerStatusreturns{ known_issues: ['ios_attribution_lag_24h'] }for one specific offer, so the agent reliably reasons about it. The other offers return clean data and the agent handles that too. - Concurrency-safe: if two tabs click Investigate at the same time, the server dedupes on
offer_id + latest_seqand streams to both.
Skip: retrieval over a doc store, embeddings, multi-agent orchestration, function-calling with arbitrary user prompts. This is a specific tool with a specific goal.
Done when:
- Clicking "Why is this stuck?" on the seeded Needs Attention offer → drawer opens → thinking streams ("The offer is in RETRY state with created_at 2h 13m ago…") →
queryOfferEventsis called visibly with correct args →queryPartnerStatusreturns the iOS lag → final answer appears, paragraph-by-paragraph, naming the partner issue and recommending a re-check → action button fires the webhook → drawer closes → card morphs to In Progress. - Deleting
ANTHROPIC_API_KEYfrom the env → the feature still works end-to-end indemoModewith a recorded streaming response. - Re-opening the drawer on an unchanged offer rehydrates instantly (no API call).
- All streaming survives a tab blur/focus cycle.
This is the demo beat the hiring panel will remember. Three Claude API features (extended thinking, tool use, streaming) stacked on top of the event log and the webhook system, all wired together in a UX that would be at home on super.com tomorrow.
Why it's sixth. Green CI on the PR is part of the pitch.
Goal. Every PR runs typecheck → vitest → next build → playwright. Main-branch builds deploy to Vercel preview automatically (already live; just confirm). A new Playwright spec exercises the Investigation Agent with the SSE flow end-to-end in demoMode.
Deliverables:
.github/workflows/ci.yml— single job onpush+pull_request:- Node 20, pnpm 10 via corepack, cache
~/.pnpm-store pnpm install --frozen-lockfilepnpm typecheckpnpm testpnpm buildpnpm exec playwright install --with-deps chromiumpnpm exec playwright test
- Node 20, pnpm 10 via corepack, cache
playwright/my-activity-v2.spec.ts— three new specs in addition to the v1 smokes:- SSE: fire a partner webhook via
trpc.deals.simulatePartnerWebhook, assert the card transitions within 2s without a full-page reload. - Investigation: open the drawer on a Needs Attention offer in
demoMode, assert thequeryPartnerStatustool call appears, assert the final answer contains the seeded issue id. - Resume: start an SSE subscription, disconnect + reconnect the fetch mock, assert missed events replay via
Last-Event-ID.
- SSE: fire a partner webhook via
- Playwright tests start
pnpm devwithANTHROPIC_API_KEY=""so CI always usesdemoMode— deterministic and free. - README gains a CI badge + a 30-second "what CI runs" line.
Skip: visual-regression diffing, matrix OS tests, a separate lint job, GitHub Actions caching beyond pnpm. One gate, one machine.
Done when:
- Open a PR, CI turns green in ≤4 minutes.
- Deliberately break the event-log projection → CI fails with a readable assertion from the Playwright SSE spec. Revert, green again.
Why it's seventh. The difference between a working demo and a memorable one is ten minutes of polish and a script.
Goal. The preview URL is demoable cold to a stranger. A dedicated docs/demo-script.md walks through the four pillars as a seven-minute story. README has a "watch this" section pointing to the live URL with the exact scenario links in the right order.
Deliverables:
docs/demo-script.md:- Act 1: The baseline (60s). Open
/earn/activity?scenario=mixed. Point out the four states, the "Live" pill, the freshness label. "Everything you saw in v1 still works." - Act 2: Partner push (90s). Open the admin panel. Fire
attribution_verifiedon the Needs Attention card. The audience watches it morph live. "In production that POST comes from the partner. Here I'm faking it in front of you." - Act 3: Multi-tab sync (30s). Open a second tab. Fire another event. Both tabs update. "One SSE, many tabs, via BroadcastChannel. Follower tabs don't hold connections."
- Act 4: Investigation (3m). Pick the iOS-lag seeded offer. Click "Why is this stuck?". Narrate the thinking as it streams, call out each tool call, read the final answer, click the action button, watch the card resolve.
- Act 5: Under the hood (60s). Open the Live Ops console (if Phase 9 shipped) or DevTools Network to show the SSE connection. Point at the event log. "Everything you just saw is a linear replay of 11 events."
- Act 6: What would change in prod (30s). Name the three things I'd swap: in-memory log → Postgres (or Vercel Queues); demo admin panel → real partner HMAC; scripted tools → real attribution-service clients. "Everything else is the same shape."
- Act 1: The baseline (60s). Open
README.mdupdates:- New "v2" section with an animated GIF (or Loom) of the Investigation Agent streaming.
- Scenario URLs listed in demo order.
- One-line mention of each of the four pillars with a link to the relevant source file.
- Motion pass: review Phase 4's timings with fresh eyes, trim any >500ms transition.
- Error states: SSE offline → pill + a quiet banner "Working offline — changes will sync when you reconnect." Investigation demoMode → a small subtitle "(Running in demo mode — real investigations require ANTHROPIC_API_KEY.)"
- Empty fixtures:
?scenario=rewardedalready shows only Rewarded; confirm the connection pill and Investigation button don't appear when they'd be meaningless.
Skip: Loom, OG images, a custom domain, a marketing landing. The URL + the demo script is the deliverable.
Done when:
- A stranger can open the preview URL + scroll
docs/demo-script.mdand reach the end in 7 minutes without a guide. - Each act's demo moment works on the first try, 5 times in a row.
Goal. The existing createActivityOfferPlacement / updateActivityOfferPlacement / deleteActivityOfferPlacement mutations become optimistic. When a concurrent SSE event arrives with a conflicting change, the UI detects the conflict and surfaces a merge affordance.
Deliverables:
src/lib/realtime/optimistic.ts— thin wrapper over React Query'sonMutate/onErrorthat tags optimistic diffs with a client-sidemutation_id.applyEventgains conflict detection: if an incomingoffer_updatedevent touches a field currently optimistic on the client, mark itconflicted: true.ConflictToast— MUISnackbar, stacked: "Someone else updated this offer. [View diff] [Keep my change] [Use theirs]."- Playwright spec: two concurrent mutations from two tabs → exactly one toast in the losing tab.
Skip: CRDT, operational transforms, three-way merge. Last-writer-wins with a user-visible choice is enough.
Done when:
- Two tabs, same offer. Tab A clicks the "Re-check" action; tab B fires a conflicting partner webhook. Tab A sees a toast within 1s. Clicking "Keep my change" re-submits; "Use theirs" discards.
Goal. Cmd+Shift+D opens a floating panel at the bottom of the viewport with:
- SSE connection state: status, reconnect count, total events received, last event id/time.
- Per-procedure latency: in-memory histogram (count + p50 + p95) for
getActivityOfferPlacements,createActivityOfferPlacement,updateActivityOfferPlacement,deleteActivityOfferPlacement,investigateOffer,getActivityEvents. Sampled from a client-side interceptor around every tRPC call. - Web Vitals: LCP, INP, CLS — live values, coloured by the official thresholds.
- Event log tail: last 20
ActivityEvents with kind + actor + offer title.
Deliverables:
src/features/ops/LiveOpsConsole.tsx— MUIDraweranchored bottom; four tabs; keyboard shortcut via a global effect.src/lib/telemetry/histogram.ts— minimal in-memory TDigest-alike; returns{ n, p50, p95 }.- tRPC link:
createTRPCNextgets a custom link that wraps every call with aperformance.now()pair and feeds the histogram. src/lib/telemetry/webVitals.ts— mountsweb-vitalsobservers, pushes values into a context.- Console collapses to a 40px dockbar when closed; remembers state in
sessionStorage.
Skip: exporting to a real APM, multi-tab aggregation, sampling / sampling rates. One tab, right now, good enough.
Done when:
Cmd+Shift+Dopens the console. Fire 5 partner webhooks in quick succession — the SSE tab shows 5 events arriving, the latency tab shows a histogram with visible p50/p95 bars, Web Vitals shows a stable LCP and an INP that spikes on the click.
Goal. A scrubber at the top of the Live-Ops Console lets you slide through the event log. The activity page re-renders the state at that point in time. A "Copy replay URL" button deep-links to that timestamp.
Deliverables:
src/features/ops/TimeTravelScrubber.tsx— MUISliderbound toseq. Dragging pauses SSE application; releasing resumes from live.deals.getActivityOfferPlacementsgains an optionalat_seqinput. When present, it projects the state from the event log up toseq = at_seqrather than the latest.- React Query cache key includes
at_seqso scrubbing is free on revisits. ?at=<seq>URL param makes the scrubbed state shareable.
Skip: scrubbing into the future (obviously), editing events, branching timelines.
Done when:
- Fire a sequence of webhooks. Open the scrubber. Slide back to before the webhook. Card states revert. "Copy replay URL" produces a URL that, opened in an incognito window, shows the same historical state.
| Phase | Work | Budget | Cumulative |
|---|---|---|---|
| 1 | Event-sourced ledger | 1.00h | 1.00h |
| 2 | SSE broadcast layer + multi-tab | 1.25h | 2.25h |
| 3 | Partner webhook simulator | 1.00h | 3.25h |
| 4 | Live card motion | 0.75h | 4.00h |
| 5 | Investigation Agent (Claude) | 3.00h | 7.00h |
| 6 | CI/CD + new Playwright specs | 0.75h | 7.75h |
| 7 | Polish + demo script | 1.00h | 8.75h |
| — | Core buffer | 0.25h | 9.00h |
| 8 | Stretch: Optimistic + conflict | 1.25h | 10.25h |
| 9 | Stretch: Live-Ops Console | 1.50h | 11.75h |
| 10 | Stretch: Time-travel scrubber | 1.50h | 13.25h |
The must-ship set fits in 9h with a 25 min core buffer. Stretch phases go in priority order: Live-Ops Console first (biggest wow-per-hour after Phase 5), then Time-travel, then Optimistic last (most fragile, easiest to ship a broken version of).
Expected landing zone: Phases 1–7 + Phase 9 = 10.5h, leaves 1.5h for demo rehearsal and preview-URL smoke testing. That's the target.
Apply in order. Each recovers 45–90 min without collapsing the story.
- Hour 8, behind → Phase 5 drops extended thinking. Keep streaming + tool use; drop
thinking: { enabled }. Saves ~20 min of UI + testing. The drawer still looks like magic. - Hour 9, behind → Phase 5 drops the
queryUserOfferHistorytool. Two tools is still "tool use." Saves ~25 min. - Hour 10, behind → skip Phase 9 (Live-Ops Console) entirely. The demo carries itself without it. The connection pill in the header is enough "observability" for the pitch.
- Hour 11, behind → Phase 6 reduces to typecheck + vitest only. Drop the Playwright specs from CI; keep them local. Saves ~20 min of debugging GHA.
- Hour 11.5, behind → skip motion polish pass (tail of Phase 4). Motion still works; just not hand-tuned.
- Hour 11.75, behind → demo script becomes bullet points in README. Skip the standalone markdown file.
Do not cut in this order — these stay no matter what:
- SSE + webhook → event → card transition (Phase 2 + 3 together)
- Investigation Agent answering at least one offer in demoMode (Phase 5 core)
- Green CI badge on the PR (Phase 6 reduced)
- A README that tells a stranger what to click (Phase 7 reduced)
If any of those four are in doubt, stop new work and consolidate.
- Preview URL is green on a merged
mainbranch. CI is green. - Opening
?scenario=mixed&admin=1in two tabs + firing a partner webhook shows both cards animating to their new state within 1s, without a page reload. - Clicking "Why is this stuck?" on a seeded Needs Attention offer streams visible thinking, at least two tool calls with visible args + results, and a final markdown answer with a recommended next step.
- Clicking the recommended-next-step button fires a real webhook and resolves the card.
- Killing the server and bringing it back reconnects the SSE and replays missed events via
Last-Event-ID. - Every event the UI shows is also visible in
deals.getActivityEvents— the audit log is the source of truth. -
pnpm testpasses (all v1 tests + ≥12 new v2 tests).pnpm exec playwright testpasses. -
docs/demo-script.mdexists and walks the four acts in order. - README has a v2 section linking Phase 2 / 3 / 5 source files.
If Phase 9 or 10 lands, their own "Done when" criteria apply as additive.
Seven minutes, one take. Every beat here is an acceptance criterion from the phases above.
0:00 — Land on /earn/activity?scenario=mixed. "This is v1. Four states, manual refresh."
0:30 — Point at the header. "See the green 'Live' pill? That's an SSE connection. One connection per browser, shared across tabs via BroadcastChannel."
1:00 — Open the admin panel (?admin=1). Fire attribution_verified on the Needs Attention card. The card morphs. The pill pulses. "No refresh, no refetch. That request went to /api/webhooks/partner — HMAC-signed, idempotent — became an event in the log, fanned out via SSE, and React Query applied it in place."
1:45 — Open a second tab with the same URL. "Both tabs are looking at the same log. Only one of them is holding the SSE connection."
2:15 — Click 'Why is this stuck?' on the seeded offer.
- Drawer opens. "This is powered by
claude-sonnet-4-6with extended thinking, tool use, and streaming." - Thinking streams. "You're watching the model reason — that's
thinkingblocks." queryOfferEventstool call appears. "It's querying our event log. Same procedure you'd call from a production service."queryPartnerStatusreturns the iOS-lag issue. "The partner status check surfaces a known issue."- Final answer streams. Point at the "Recommended next step" card.
5:00 — Click the action button. The webhook fires; the card resolves to In Progress; the drawer closes. "End to end — AI recommendation, user action, partner webhook, event log, SSE fan-out, UI update."
5:30 — Open Live Ops console (Cmd+Shift+D). (If Phase 9 shipped.) "p50, p95, Web Vitals, every event that's crossed the wire. Nothing is sampled — this is every call I've made."
6:00 — Show docs/demo-script.md + the PR description. "Everything you saw is in four PRs, all green CI, 12 hours start to finish."
6:45 — The one-liner. "The shape is a miniature version of what a rewards platform actually runs. Swap the in-memory log for Postgres or Vercel Queues, swap the admin panel for real partner HMACs, and you're at production."
| Risk | Mitigation |
|---|---|
| Anthropic API rate limits or key revocation mid-demo | demoMode fallback with a pre-recorded streaming response. The demo never hits a dead end; the README flags the fallback honestly. |
| SSE disconnects under Fluid Compute cold starts or proxy idle timeouts | 30s heartbeat + Last-Event-ID replay. Acceptance criterion in Phase 2 exercises a restart. |
| Event log grows unboundedly over a long demo | Capped at 1,000 events per offer in-memory; older events are dropped with a warning log (not relevant to the demo, but keeps the server honest). |
| Investigation drawer feels slow because streaming is visibly token-by-token | Extended thinking is budget-capped at 2000 tokens; final answer is capped at 1024. First visible token lands ≤2s in practice with claude-opus-4-7. |
| Optimistic mutations (Phase 8) introduce visible flicker on conflict | Out of scope for core demo; Phase 8 is stretch. Core demo uses pessimistic mutations + SSE state. |
| Admin panel leaks into prod and a real user fires a webhook | Panel is gated on ?admin=1 AND NODE_ENV !== 'production' OR an allowlisted IP. Belt + suspenders. |
| Playwright SSE spec is flaky on GHA | Use trpc.deals.simulatePartnerWebhook (tRPC), not a raw fetch. Assert via locator.waitFor with a generous 5s timeout. |
| Scope creep into real analytics or real auth | Every time it tempts you, note it as additive scope. The demo is the scope. |
Three signals the panel will pick up:
1. It takes the shape of the real platform. Event-sourced state + partner webhooks + real-time fan-out + AI explainability is literally the backend Super runs. The demo isn't a mock of the UI; it's a miniature of the system. In a day, by one person.
2. It combines four hard skills without dropping any. Distributed state (event log + replay), transport (SSE + backoff + multi-tab), LLM product thinking (tool use + streaming + extended thinking + prompt caching), and platform engineering (CI + e2e + structured telemetry). Any one of them is a full engineer's job. Shipping all four, wired together, on a deadline, is the signal that everyone on the panel is actually looking for.
3. It respects the constraints. No stack swaps. No new routers that weren't in the HAR. No vendor lock-in the team didn't choose. No "while I'm here" refactors of v1. It reads as someone who can ship into an unfamiliar codebase without blowing up the work that came before.
That's the pitch. Now go build it.
- v1 execution plan (shipped): my-activity-implementation-phases.md
- Product/investigation plan: my-activity-page-progress-improvement-plan.md
- Architecture context: ../super-architecture.md
- Agent guardrails: ../../CLAUDE.md
End of v2 execution plan.
{ "partner_slug": "gokart", "event_name": "milestone_completed", // or "attribution_verified" | "attribution_failed" | "reward_paid" "offer_id": "...", "event_id": "...", // for milestone_completed, the events[].id "idempotency_key": "evt_ab12cd34", "signature": "sha256=..." // HMAC of body with a per-partner secret from env }