Skip to content

feat: warn delegators when an orchestrator's reward cut is rising sharply#682

Open
rickstaa wants to merge 4 commits into
mainfrom
feat/bait-and-switch-warning
Open

feat: warn delegators when an orchestrator's reward cut is rising sharply#682
rickstaa wants to merge 4 commits into
mainfrom
feat/bait-and-switch-warning

Conversation

@rickstaa
Copy link
Copy Markdown
Member

@rickstaa rickstaa commented May 27, 2026

Summary

  • Adds useOrchestratorRewardCutSpike — pure heuristic plus React hook that flags an orchestrator when their reward cut had any ≥80% upward swing within any rolling 7-day window in the last 180 days.
  • Augments the existing cutChangeNotice in the delegation widget so it renders as a yellow warning with dynamic copy (X ago, A% → B%) when the heuristic fires. Falls back to the existing neutral info banner otherwise.
  • Shares the useTranscoderUpdateEventsQuery Apollo cache with useOrchestratorCutHistory, so the warning detection adds zero extra network requests.

Closes #681.

Visual

image

Test plan

  • pnpm typecheck passes.
  • Self-delegation: no banner.
  • showApproveFlow and final-delegate branches both render the notice in the same position.

Manual verification (14 orchestrators)

Verified on the preview deployment for this PR. You can go to https://explorer-arbitrum-21f45kehd-livepeer-foundation.vercel.app?_vercel_share=sBPQHDRcO22Lg2abbMhsTnatlAdk8zKL to check the addresses below.

Correctly flagged (9):

Correctly not flagged (3):

Summary by CodeRabbit

  • New Features

    • Added detection and visual warnings for significant orchestrator reward cut increases, displaying before/after percentages and timing information.
  • Chores

    • Updated build configuration files to ignore development-related directories.

…rply

Render the existing delegation widget banner as a yellow warning when
an orchestrator's reward cut had any >=50 percentage point upward swing
within any rolling 7-day window in the last 180 days. Surfaces sharp
reward cut increases at the point of delegation so delegators can review
the history before committing stake.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 27, 2026 17:33
@rickstaa rickstaa requested a review from ECWireless as a code owner May 27, 2026 17:33
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
explorer-arbitrum-one Ready Ready Preview, Comment Jun 4, 2026 11:17am

Request Review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a delegator-facing warning when an orchestrator’s reward cut has recently increased sharply (≥50 percentage points within any rolling 7-day window over the last 180 days), surfacing this risk directly in the delegation flow.

Changes:

  • Introduces useOrchestratorRewardCutSpike plus a pure helper (findRecentRewardCutSpike) to detect recent reward-cut spikes from TranscoderUpdateEvent history.
  • Updates the Delegation widget notice to render a yellow warning with relative timing and from/to cut percentages when a spike is detected (neutral info banner otherwise).
  • Aligns cut-history calculations to use PERCENTAGE_PRECISION_MILLION and expands ignores for local tooling dirs (.playwright-mcp, .vscode) in eslint/prettier/git.

Reviewed changes

Copilot reviewed 5 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
hooks/useOrchestratorRewardCutSpike.tsx Adds spike-detection heuristic and hook backed by useTranscoderUpdateEventsQuery.
hooks/useOrchestratorCutHistory.tsx Refactors input typing and replaces hardcoded 1_000_000 with PERCENTAGE_PRECISION_MILLION.
components/DelegatingWidget/Delegate.tsx Replaces the static cut notice with a spike-aware warning banner.
eslint.config.mjs Ignores .playwright-mcp/** and .vscode/** in ESLint.
.prettierignore Ignores .playwright-mcp.
.gitignore Ignores .playwright-mcp/.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread hooks/useOrchestratorRewardCutSpike.tsx
Comment thread hooks/useOrchestratorRewardCutSpike.tsx
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

Adds reward-cut spike detection and a hook, refactors the cut-history hook's input/precision, integrates a conditional warning banner into the Delegate UI, adds unit tests for spike detection, and updates Git/Prettier/ESLint ignores for .playwright-mcp.

Changes

Reward Cut Spike Warning

Layer / File(s) Summary
Spike detection algorithm and hook
hooks/useOrchestratorRewardCutSpike.tsx
Exports REWARD_CUT_SPIKE_PP, ROLLING_WINDOW_DAYS, WARNING_WINDOW_DAYS, types CutEvent/RewardCutSpike, findRecentRewardCutSpike (searches newest→oldest for up-only swings within a rolling window), and useOrchestratorRewardCutSpike (Apollo query + memoized detection).
Cut history hook flexibility
hooks/useOrchestratorCutHistory.tsx
Changes input to `OrchestratorRef
Delegation warning UI integration
components/DelegatingWidget/Delegate.tsx
Adds CutChangeNotice component that uses the spike hook and replaces the previous static pre-delegation banner with <CutChangeNotice orchestratorId={to} />, rendering a neutral info or yellow warning with spike details.

Tests

Layer / File(s) Summary
Spike detection unit tests
hooks/useOrchestratorRewardCutSpike.test.ts
New Jest suite covering empty/single-event cases, multiple spike scenarios, threshold boundary, negative cases, cutoff behavior, and most-recent spike selection.

Development Tooling Configuration

Layer / File(s) Summary
Playwright MCP tool ignores
.gitignore, .prettierignore, eslint.config.mjs
Adds .playwright-mcp/ to Git and Prettier ignore lists and .playwright-mcp/** to ESLint globalIgnores.

Sequence Diagram

sequenceDiagram
  participant CutChangeNotice
  participant useOrchestratorRewardCutSpike
  participant TranscoderUpdateEventsQuery
  participant findRecentRewardCutSpike
  CutChangeNotice->>useOrchestratorRewardCutSpike: call(orchestratorId)
  useOrchestratorRewardCutSpike->>TranscoderUpdateEventsQuery: fetch events (first:1000, order: Timestamp_DESC)
  TranscoderUpdateEventsQuery-->>useOrchestratorRewardCutSpike: events[]
  useOrchestratorRewardCutSpike->>findRecentRewardCutSpike: pass events
  findRecentRewardCutSpike-->>useOrchestratorRewardCutSpike: RewardCutSpike | null
  useOrchestratorRewardCutSpike-->>CutChangeNotice: RewardCutSpike | null
Loading

Estimated code review effort:
🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues:

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: warn delegators when an orchestrator's reward cut is rising sharply' directly and clearly summarizes the main feature added by this PR: a warning system for delegators when orchestrators' reward cuts spike upward.
Description check ✅ Passed The PR description comprehensively covers all required template sections: Summary (with specifics), Type of Change (feat), Related Issue (Closes #681), Changes Made (three detailed bullet points), Testing (all checkboxes marked, manual verification on 14 orchestrators documented), Impact/Risk (implicitly low-risk, delegator warning feature), and Additional Notes (visual screenshots, test plan, deployment link).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bait-and-switch-warning

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hooks/useOrchestratorRewardCutSpike.tsx`:
- Around line 88-94: The query in useTranscoderUpdateEventsQuery is fetching the
oldest 1000 updates because it sets first: 1000 and orderDirection:
OrderDirection.Asc, which can omit the last 180 days; change the pagination to
fetch newest-first by setting orderDirection to OrderDirection.Desc (keep
orderBy: TranscoderUpdateEvent_OrderBy.Timestamp and first as needed) and/or add
a server-side time filter to the variables.where (e.g., include a timestamp
cutoff condition) so findRecentRewardCutSpike always receives events from the
most recent 180-day window.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3f5f448b-6ddc-4ee5-b16f-fba6721d4cec

📥 Commits

Reviewing files that changed from the base of the PR and between 9959381 and 787ba64.

📒 Files selected for processing (6)
  • .gitignore
  • .prettierignore
  • components/DelegatingWidget/Delegate.tsx
  • eslint.config.mjs
  • hooks/useOrchestratorCutHistory.tsx
  • hooks/useOrchestratorRewardCutSpike.tsx

Comment thread hooks/useOrchestratorRewardCutSpike.tsx
For orchestrators with >1000 lifetime update events, Asc + first:1000 dropped
the recent 180-day window entirely. Flip both queries to Desc so the recent
slice is always present; chart hook sorts ascending in its mapping step to
preserve display order.

Co-Authored-By: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers threshold, window cutoff, up-only direction, and most-recent-spike selection.

Co-Authored-By: Copilot <175728472+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Increase REWARD_CUT_SPIKE_PP from 0.5 to 0.8 so only sharper upward
swings (≥80pp within the rolling window) trigger the bait-and-switch
warning, reducing false positives from moderate reward cut increases.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hooks/useOrchestratorRewardCutSpike.test.ts`:
- Around line 27-61: Tests reference a stale 50pp threshold; update assertions
in useOrchestratorRewardCutSpike.test.ts to use the current REWARD_CUT_SPIKE_PP
(0.8) instead of hardcoded 50/75/49 values: modify calls to
findRecentRewardCutSpike and the event rewardCut values so the spike-generating
differences match REWARD_CUT_SPIKE_PP*100 (e.g., use REWARD_CUT_SPIKE_PP*100 and
(1-REWARD_CUT_SPIKE_PP)*100 as needed) and replace expected
fromRewardCut/toRewardCut assertions to derive from REWARD_CUT_SPIKE_PP (e.g.,
expect(spike?.toRewardCut).toBe(REWARD_CUT_SPIKE_PP) and
expect(spike?.fromRewardCut).toBe(1-REWARD_CUT_SPIKE_PP) or compute percent
equivalents) so tests use the constant rather than hardcoded 50/49/75 values;
update the three failing cases around the single-event threshold, the "exactly
threshold" test, and the "below threshold" test accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 74591508-186a-4021-b5e0-9d49c15aae5b

📥 Commits

Reviewing files that changed from the base of the PR and between 787ba64 and 8db9ead.

📒 Files selected for processing (3)
  • hooks/useOrchestratorCutHistory.tsx
  • hooks/useOrchestratorRewardCutSpike.test.ts
  • hooks/useOrchestratorRewardCutSpike.tsx

Comment on lines +27 to +61
it("fires on a single-event 50pp jump", () => {
const spike = findRecentRewardCutSpike([event(10, 0), event(5, 50)], opts);
expect(spike?.fromRewardCut).toBe(0);
expect(spike?.toRewardCut).toBe(0.5);
});

it("fires on a 4x25pp climb spread over 6 days", () => {
const spike = findRecentRewardCutSpike(
[event(10, 0), event(9, 25), event(8, 50), event(7, 75), event(5, 100)],
opts
);
expect(spike?.fromRewardCut).toBe(0);
expect(spike?.toRewardCut).toBe(1);
});

it("fires on a dip-then-spike within 7 days", () => {
const spike = findRecentRewardCutSpike(
[event(30, 100), event(5, 0), event(2, 100)],
opts
);
expect(spike?.fromRewardCut).toBe(0);
expect(spike?.toRewardCut).toBe(1);
});

it("fires at exactly the 50pp threshold", () => {
expect(
findRecentRewardCutSpike([event(10, 25), event(5, 75)], opts)
).not.toBeNull();
});

it("does not fire below the 50pp threshold", () => {
expect(
findRecentRewardCutSpike([event(10, 0), event(5, 49)], opts)
).toBeNull();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Threshold assertions are stale (50pp) and now fail CI against the 80pp rule

  • Problem: Line 27, Line 51, and Line 57 encode a 50pp threshold expectation, but runtime logic now uses REWARD_CUT_SPIKE_PP = 0.8 (80pp).
  • Why it matters: CI is red (reported failures at Line 29 and Line 54), so this PR cannot merge with reliable test signal.
  • Suggested fix: Update these cases to 80pp semantics (and ideally derive assertions from REWARD_CUT_SPIKE_PP so future threshold tuning doesn’t break tests again).
Proposed patch
 import {
   type CutEvent,
   findRecentRewardCutSpike,
+  REWARD_CUT_SPIKE_PP,
 } from "./useOrchestratorRewardCutSpike";
@@
-  it("fires on a single-event 50pp jump", () => {
-    const spike = findRecentRewardCutSpike([event(10, 0), event(5, 50)], opts);
+  it("fires on a single-event 80pp jump", () => {
+    const spike = findRecentRewardCutSpike([event(10, 0), event(5, 80)], opts);
     expect(spike?.fromRewardCut).toBe(0);
-    expect(spike?.toRewardCut).toBe(0.5);
+    expect(spike?.toRewardCut).toBe(0.8);
   });
@@
-  it("fires at exactly the 50pp threshold", () => {
+  it("fires at exactly the configured threshold", () => {
     expect(
-      findRecentRewardCutSpike([event(10, 25), event(5, 75)], opts)
+      findRecentRewardCutSpike(
+        [event(10, 0), event(5, REWARD_CUT_SPIKE_PP * 100)],
+        opts
+      )
     ).not.toBeNull();
   });
@@
-  it("does not fire below the 50pp threshold", () => {
+  it("does not fire below the configured threshold", () => {
     expect(
-      findRecentRewardCutSpike([event(10, 0), event(5, 49)], opts)
+      findRecentRewardCutSpike(
+        [event(10, 0), event(5, REWARD_CUT_SPIKE_PP * 100 - 1)],
+        opts
+      )
     ).toBeNull();
   });
🧰 Tools
🪛 GitHub Actions: CI / 0_lint-and-test.txt

[error] 29-29: Jest assertion failed in test 'findRecentRewardCutSpike › fires on a single-event 50pp jump'. Expected spike?.fromRewardCut to be 0, but received undefined.


[error] 54-54: Jest assertion failed in test 'findRecentRewardCutSpike › fires at exactly the 50pp threshold'. Expected result not to be null, but received null.

🪛 GitHub Actions: CI / lint-and-test

[error] 29-29: Jest test failed in 'findRecentRewardCutSpike › fires on a single-event 50pp jump'. Expected spike?.fromRewardCut to be 0, received undefined.


[error] 54-54: Jest test failed in 'findRecentRewardCutSpike › fires at exactly the 50pp threshold'. Expected result not to be null, but received null.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/useOrchestratorRewardCutSpike.test.ts` around lines 27 - 61, Tests
reference a stale 50pp threshold; update assertions in
useOrchestratorRewardCutSpike.test.ts to use the current REWARD_CUT_SPIKE_PP
(0.8) instead of hardcoded 50/75/49 values: modify calls to
findRecentRewardCutSpike and the event rewardCut values so the spike-generating
differences match REWARD_CUT_SPIKE_PP*100 (e.g., use REWARD_CUT_SPIKE_PP*100 and
(1-REWARD_CUT_SPIKE_PP)*100 as needed) and replace expected
fromRewardCut/toRewardCut assertions to derive from REWARD_CUT_SPIKE_PP (e.g.,
expect(spike?.toRewardCut).toBe(REWARD_CUT_SPIKE_PP) and
expect(spike?.fromRewardCut).toBe(1-REWARD_CUT_SPIKE_PP) or compute percent
equivalents) so tests use the constant rather than hardcoded 50/49/75 values;
update the three failing cases around the single-event threshold, the "exactly
threshold" test, and the "below threshold" test accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Warn delegators when an orchestrator's reward cut is rising sharply

2 participants