feat: partner revenue breakout (payout-accurate) + net-of-partner affiliate revenue - #26
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…partner endpoint Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…api-key auth & worktree Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ unmatched fallback Splits a matched provider fee event by partnerBps/affiliateBps (provider USD is source of truth), crediting the partner and reducing ShapeShift's net fee accordingly. For partner swaps with no matching fee event (e.g. chainflip emits no txHash), emits a synthetic negative fee on the mapped service and flags it unreconciled; unmapped swappers are still credited to byPartner without a synthetic fee. Adds optional `synthetic` to `Fees`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er swaps Fetches the affiliate registry and all partner swaps (cursor-paginated) from swap-service, mapping enriched rows to PartnerSwapRow for downstream settlement building. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… with graceful fallback Wire buildSettlement/fetchPartnerSwaps into getAffiliateRevenue so byService/byDate/totals reflect ShapeShift-NET numbers after peeling partner-attributed swaps. Extracts fee collection into collectFees() and settlement into settle() (graceful fallback to raw fees on swap-service failure) so B5 can reuse both. Synthetic negative-adjustment fees are excluded from fee counts and the per-asset breakdown but still reduce totalUsd/ byService/byServiceVolume. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds AffiliateRevenue.getPartnerRevenue, reusing the collectFees/settle helpers from B4, with a graceful fallback to affiliates: [] if the registry fetch fails. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds PartnerRevenue/PartnerRevenueResponse types, a fetchPartnerRevenue api client, and a usePartnerRevenue hook (with an enabled flag so the Partners tab only fetches when active), mirroring the existing affiliate revenue equivalents. Also adds the unreconciled field to AffiliateRevenueResponse to match the updated backend response. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a sortable PartnerBreakdown table (revenue/volume/swaps, partner payouts total, unreconciled badge) and a dependency-free Revenue/Partners tab nav in App.tsx. The Revenue view is preserved unchanged and now notes it is net of partner payouts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…partner ratio Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…'t refetch The partner query was gated on `enabled: tab === 'partners'`, so the first Partners click triggered a fresh fetch for a period already loaded. Fetch it in parallel with the revenue query and let react-query cache by date range; switching tabs now reads cache, and changing the period refetches both. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These were dated one-off QA/analysis artifacts (integration test runs, token-identification scratch, a one-time discrepancy root-cause) with no ongoing reference value and nothing linking to them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cratch - Move the design spec to docs/design/ (out of the tool-specific superpowers/ path) - Drop the implementation plan (ephemeral execution scaffolding) - gitignore docs/plans/ and .superpowers/ as local-only, uncommitted scratch Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e into its own module Relocate the partner-revenue settlement (settle/swapServiceClient/swapperServiceMap/ types) from affiliateRevenue/partnerSettlement to a sibling src/partnerRevenue/ module to separate partner-revenue concerns from affiliate-revenue; repoint imports. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…evenue routes Both /affiliate/revenue and /partner/revenue duplicated the same YYYY-MM-DD validation, timestamp conversion, and 400/500 error handling. Extract a createRevenueRoute factory + parseDateRange helper; each route file is now a thin binding to its service method. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p-service Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… util Reverts the createRevenueRoute factory (over-abstracted — hid the route structure and forced boilerplate through it). Routes keep their own explicit handlers; only the date-range validation is shared, as utils/parseDateRange. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The test only exercised an injected fake fetch (pagination plumbing), asserting little about real behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… swaps getPartnerRevenue no longer runs the affiliate provider collection + netting settlement. Partner revenue is a straight roll-up of each partner swap's partnerFeeUsd (new aggregatePartnerRevenue), independent of the ShapeShift-net revenue view. The txHash settlement (buildSettlement) is now only used by getAffiliateRevenue to peel cuts from gross fees, so its byPartner output is dropped (netFees + unreconciled only), and the orphaned swapperServiceMap module is removed. Drops the settlement-only `unreconciled` field from the partner response (backend + frontend). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…jection) Rename fetchAffiliateRegistry -> fetchAffiliates and drop its opts/fetchImpl seam (only the removed swapServiceClient test used it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…waps Remove the opts/fetchImpl seam and the now-unused FetchLike/Opts types; the only consumer was the removed swapServiceClient test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st at boot) These are required to talk to swap-service; a missing value was silently falling back to localhost / an empty key. Throw at module load instead, so a misconfiguration fails at boot rather than degrading to gross revenue at runtime. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ net reconciliation) Consolidate partner revenue into affiliateRevenue: reconcileRevenue (net deduction from gross), aggregateRevenue (affiliate + partner responses), and a shared payablePartnerFee gate. Only on-chain-verified, payable swaps (status SUCCESS + verified bps + actual affiliate-fee amount + anomaly-guarded) are counted — matching the affiliate-payouts script — so partner numbers never overstate. Non-payable/unmapped/failed-provider swaps are excluded and logged per-swap for audit. ShapeShift-net floors at 0 (never negative) and suppresses deductions when a provider failed. Move Fees + date helpers into their own modules; drop the unused `unreconciled` response field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…bles Render the partner breakdown as a section below revenue-by-asset (drop the separate Partners tab). Add sortable columns (shared SortArrow indicator, sticky headers), lazy-rendered infinite scroll capped at ~10 rows, and a thin themed scrollbar — applied to both the partner and asset breakdown tables. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address CodeRabbit review: add AbortSignal.timeout to both swap-service fetches (a stalled response no longer hangs the request) and a page cap on the cursor loop; exclude swaps with negative partnerBps as invalid rather than emitting a negative payable share. Add cap-path and negative-bps tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address CodeRabbit review: extract a shared SortHeader with role/tabIndex/ aria-sort/Enter-Space handling (used by asset + partner tables, removing the duplicated inline markup); surface partner-revenue fetch failures as an error state instead of the empty 'no data' message. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove MAX_PAGES (the fetch timeout is the meaningful guard; a correct cursor won't loop). Surface partner-revenue fetch failures as a top-of-page banner (consistent with the affiliate error) instead of an in-component message. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…vice The external per-slice labels overlapped on small slices and their uneven widths pushed the ring off-center. The legend + table (with color dots) + hover tooltip already map colors to services, so the donut is now a clean centered ring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chart cell is shorter than the table, so it top-aligned in the grid row. self-center vertically centers it against the table. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Stabilize the fee-anomaly reason (drop the per-swap deviation %) so exclusions group by reason instead of each % becoming its own bucket. - Log $ impact per reason (sorted by $), with a per-swapper breakdown that surfaces systematic gaps, and a total header. Drop the misused 'e.g. <ids>'. - Carry partnerFeeUsd on ExcludedPartnerSwap for the $ totals. - Remove a stray debug console.log in the anomaly branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…DB inspection Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tnerRevenue log) getAffiliateRevenue and getPartnerRevenue independently evaluated the same partner swaps, so non-payable exclusions were logged twice. The reconcile path is a superset (also covers unmapped/failed-provider), so keep only that log. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ciliation] Neutral tag (matches reconcilePartnerRevenue) — accurate for both partner-fee exclusions and net-only exclusions (unmapped/failed provider), unlike [PartnerRevenue] which would imply those were dropped from partner revenue. Drop the now-single-use context param. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iate bps A swap verified on-chain with verifiedBps 0 (a real, settled swap that earned $0) was excluded from the partner breakdown entirely — no swap count, no volume — because getPayablePartnerFee conflated it with an unverified swap (verifiedBps null). Treat verifiedBps === 0 as a distinct, payable-with-$0 case: it counts toward swapCount + volume but contributes $0 revenue, so the partner view matches the affiliate side without overstating anyone. The branch returns before the fee-asset/amount checks (legitimately absent when nothing was collected) and before the ÷verifiedBps share (which would divide by zero); null verifiedBps stays excluded. Hoist the partnerBps >= 0 guard above the branch so malformed swaps are rejected regardless of fee, and skip the $0 synthetic in reconcile (nothing to deduct). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/revenue-dashboard/src/api/partnerRevenue.ts (1)
3-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract shared API base URL and fetch logic to reduce duplication.
partnerRevenue.tsandaffiliateRevenue.tsnow share identicalAPI_BASE_URLguards and nearly identical fetch wrappers. Extracting a shared module (e.g.,api/client.ts) for the base URL validation and a genericapiFetch<T>(path, params)helper would eliminate this duplication and ensure timeout/error-handling conventions stay consistent across endpoints.♻️ Suggested shared client extraction
// api/client.ts const API_BASE_URL: string = import.meta.env.VITE_API_BASE_URL if (!API_BASE_URL) throw new Error('VITE_API_BASE_URL is required') export async function apiFetch<T>(path: string, params: Record<string, string>): Promise<T> { const url = new URL(path, API_BASE_URL) for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v) const response = await fetch(url.toString()) if (!response.ok) { throw new Error(`API request failed: ${response.status} ${response.statusText}`) } return (await response.json()) as T } // api/partnerRevenue.ts import { apiFetch } from './client' import type { DateRange } from '../types' import type { PartnerRevenueResponse } from '../types' export function fetchPartnerRevenue(dateRange: DateRange) { return apiFetch<PartnerRevenueResponse>('/api/v1/partner/revenue', { startDate: dateRange.startDate, endDate: dateRange.endDate, }) }🤖 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 `@apps/revenue-dashboard/src/api/partnerRevenue.ts` around lines 3 - 18, The API base URL guard and fetch wrapper in fetchPartnerRevenue are duplicated and should be centralized with the matching logic in affiliateRevenue. Move the VITE_API_BASE_URL validation and the shared request/error handling into a reusable api client module (for example, api/client.ts) and expose a generic apiFetch<T>(path, params) helper. Update fetchPartnerRevenue to use the shared helper instead of constructing the URL and handling the response inline, so both revenue endpoints rely on the same API_BASE_URL and fetch conventions.
🤖 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 `@apps/revenue-api/src/utils/date.ts`:
- Around line 41-56: The parseDateRange function in date utilities accepts
reversed ranges because it only checks format and NaN, so add a guard after
computing startTimestamp and endTimestamp to reject cases where startTimestamp
is greater than endTimestamp. Update the DateRange validation path in
parseDateRange to return an error for reversed input, and make sure downstream
callers such as getDateRange continue to rely on the corrected startDate/endDate
ordering.
In `@apps/revenue-dashboard/src/components/SortHeader.tsx`:
- Around line 22-26: The SortHeader header cell is overriding its native
columnheader semantics by setting role="button" directly on the <th>, which also
makes aria-sort invalid. Update the SortHeader component so the <th> keeps the
aria-sort and header semantics, and move the interactive behavior into a nested
<button> element inside the <th>, including the click and keyboard handlers
currently attached to the header cell.
---
Nitpick comments:
In `@apps/revenue-dashboard/src/api/partnerRevenue.ts`:
- Around line 3-18: The API base URL guard and fetch wrapper in
fetchPartnerRevenue are duplicated and should be centralized with the matching
logic in affiliateRevenue. Move the VITE_API_BASE_URL validation and the shared
request/error handling into a reusable api client module (for example,
api/client.ts) and expose a generic apiFetch<T>(path, params) helper. Update
fetchPartnerRevenue to use the shared helper instead of constructing the URL and
handling the response inline, so both revenue endpoints rely on the same
API_BASE_URL and fetch conventions.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: bb92d9cd-4586-40ed-8ea2-00903ff70578
📒 Files selected for processing (21)
apps/revenue-api/src/affiliateRevenue/aggregateRevenue.test.tsapps/revenue-api/src/affiliateRevenue/aggregateRevenue.tsapps/revenue-api/src/affiliateRevenue/cache.tsapps/revenue-api/src/affiliateRevenue/index.tsapps/revenue-api/src/affiliateRevenue/payablePartnerFee.test.tsapps/revenue-api/src/affiliateRevenue/payablePartnerFee.tsapps/revenue-api/src/affiliateRevenue/reconcileRevenue.test.tsapps/revenue-api/src/affiliateRevenue/reconcileRevenue.tsapps/revenue-api/src/affiliateRevenue/swapServiceClient.tsapps/revenue-api/src/affiliateRevenue/types.tsapps/revenue-api/src/routes/affiliateRevenue.tsapps/revenue-api/src/routes/partnerRevenue.tsapps/revenue-api/src/utils/date.tsapps/revenue-dashboard/src/App.tsxapps/revenue-dashboard/src/api/affiliateRevenue.tsapps/revenue-dashboard/src/api/partnerRevenue.tsapps/revenue-dashboard/src/components/AssetBreakdown.tsxapps/revenue-dashboard/src/components/PartnerBreakdown.tsxapps/revenue-dashboard/src/components/ServiceBreakdown.tsxapps/revenue-dashboard/src/components/SortHeader.tsxapps/revenue-dashboard/src/components/TotalVolume.tsx
🚧 Files skipped from review as they are similar to previous changes (13)
- apps/revenue-dashboard/src/App.tsx
- apps/revenue-api/src/routes/partnerRevenue.ts
- apps/revenue-api/src/routes/affiliateRevenue.ts
- apps/revenue-api/src/affiliateRevenue/reconcileRevenue.test.ts
- apps/revenue-api/src/affiliateRevenue/aggregateRevenue.test.ts
- apps/revenue-api/src/affiliateRevenue/swapServiceClient.ts
- apps/revenue-api/src/affiliateRevenue/cache.ts
- apps/revenue-api/src/affiliateRevenue/types.ts
- apps/revenue-dashboard/src/components/AssetBreakdown.tsx
- apps/revenue-dashboard/src/components/PartnerBreakdown.tsx
- apps/revenue-api/src/affiliateRevenue/reconcileRevenue.ts
- apps/revenue-api/src/affiliateRevenue/aggregateRevenue.ts
- apps/revenue-api/src/affiliateRevenue/index.ts
Guard against startTimestamp > endTimestamp so a reversed range fails validation instead of silently yielding empty/odd results downstream. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The chainId prefix + mapped value already convey the chain grouping, and the dailyAsset/globalAsset names already say what each block aggregates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
role="button" on the <th> was overriding its native columnheader role, which also invalidated the adjacent aria-sort. Nest a native <button> for the click/keyboard interaction instead, keeping aria-sort valid on the th. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Show the partner error banner with its message and suppress the Revenue by Partner section on error, so a failed fetch is no longer rendered as an empty "No partner revenue in this range" range. Also name the query-result variables with a Query suffix rather than aliased destructures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
Adds partner revenue reporting and nets each partner's share out of ShapeShift affiliate revenue, sourced from swap-service and aligned to what the affiliate-payouts script actually pays.
GET /api/v1/partner/revenue— per-partner revenue/volume/swaps, sourced from swap-service/v1/affiliate/swaps.getAffiliateRevenuenow reports ShapeShift-NET — each partner's realized share (USD + on-chain crypto) is deducted from gross provider fees, attributed to the affiliate-fee asset.Payout accuracy (the important part)
A single shared predicate,
getPayablePartnerFee, gates both the net deduction and the partner view. A swap only counts when it mirrors the payout script:status === 'SUCCESS', verified on-chain, with a real on-chain affiliate-fee amount and verified bps;Non-payable / unmapped / failed-provider swaps are excluded — never silently — and logged per-swap (swapId + reason) for audit. ShapeShift-net floors at 0 (never negative) and suppresses deductions when a provider failed (no deducting against absent gross). USD figures from swap-service are treated as strings and converted with BigNumber (fixes a latent string-concat bug).
Frontend
SortArrow, sticky headers), lazy-rendered infinite scroll capped at ~10 rows, thin themed scrollbar.Tests / checks
vite buildclean.Known follow-up (swap-service)
The payability gate is reconstructed locally from the collapsed
feeUsd/partnerFeeUsd, so it can diverge from the payout for swaps whose on-chain fee exists but isn't USD-priceable (e.g. Relay third-asset routes). The clean fix is swap-service exposing a payout-basispayablePartnerFee*field; the dashboard then drops the reconstruction and matches payouts 1:1. Spec drafted separately.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes