feat: warn delegators when an orchestrator's reward cut is rising sharply#682
feat: warn delegators when an orchestrator's reward cut is rising sharply#682rickstaa wants to merge 4 commits into
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
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
useOrchestratorRewardCutSpikeplus a pure helper (findRecentRewardCutSpike) to detect recent reward-cut spikes fromTranscoderUpdateEventhistory. - 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_MILLIONand 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.
📝 WalkthroughWalkthroughAdds 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 ChangesReward Cut Spike Warning
Tests
Development Tooling Configuration
Sequence DiagramsequenceDiagram
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
Estimated code review effort: Possibly related issues:
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
.gitignore.prettierignorecomponents/DelegatingWidget/Delegate.tsxeslint.config.mjshooks/useOrchestratorCutHistory.tsxhooks/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.
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
hooks/useOrchestratorCutHistory.tsxhooks/useOrchestratorRewardCutSpike.test.tshooks/useOrchestratorRewardCutSpike.tsx
| 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(); | ||
| }); |
There was a problem hiding this comment.
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_PPso 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.
Summary
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.cutChangeNoticein 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.useTranscoderUpdateEventsQueryApollo cache withuseOrchestratorCutHistory, so the warning detection adds zero extra network requests.Closes #681.
Visual
Test plan
pnpm typecheckpasses.showApproveFlowand 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
Chores