Skip to content

fix(mayachain): rewrite tracker on midgard, replacing unchained affiliate indexer - #32

Merged
kaladinlight merged 4 commits into
mainfrom
mayachain-midgard
Aug 3, 2026
Merged

fix(mayachain): rewrite tracker on midgard, replacing unchained affiliate indexer#32
kaladinlight merged 4 commits into
mainfrom
mayachain-midgard

Conversation

@kaladinlight

@kaladinlight kaladinlight commented Jul 31, 2026

Copy link
Copy Markdown
Member

Description

Rewrites the mayachain affiliate revenue tracker to source fees from Maya Midgard (/v2/actions?affiliate=ssmaya via our unchained proxy) instead of the unchained affiliate fee indexer endpoint, which is being removed (shapeshift/unchained#1285). Also patches the thorchain tracker for the same midgard timestamp boundary bug found in review.

Motivation

The unchained indexer drastically underreported: 124 fees all-time vs 379 actually on-chain (~$21.6k total). It required the affiliate outbound and swap event in the same block (missing streaming swaps — including the largest fees on record), its history was capped by the node's block-search index (nothing before 2025-07-04), and websocket gaps had no backfill.

Design notes (all verified against the live API)

  • Pagination: pages descending under the timestamp (upper bound, unix seconds) param only, stopping once a page's oldest action predates the window, with a client-side [start, end] filter. Maya Midgard's fromTimestamp expects nanoseconds (unlike THORChain's seconds — seconds are read as ~1970 and return from genesis) and pages unstably combined with timestamp, so it's avoided entirely.
  • Timestamp boundary (both trackers): midgard's timestamp param cuts at exactly end*1e9 ns, silently dropping fees in the final second of each cacheable day (and permanently caching them as absent). Both trackers now pass end+1 with a client-side trim. Confirmed live on both midgards: timestamp=S excludes an action at S.xxx ns, timestamp=S+1 includes it, fromTimestamp is inclusive at S.
  • Fee extraction: the affiliate: true out entry's CACAO amount (1e10 base units), in[0].txID as txHash, date (ns) → seconds.
  • Pricing: historical CACAO spot from USDC pool depth history (assetPriceUSD / assetPrice per interval), replacing current-price enrichment — consistent with the thorchain tracker's /history/rune approach. Cross-checked against Midgard's own per-swap USD prices (~1-2% agreement at daily granularity) and validated at the earliest ssmaya fee (2025-06-26).

Validation

  • 30-day mayachain window: 24 fees, 63,412 CACAO, $7,459 historical USD, $1.27M true volume — fee÷bps derived volume agrees within 1.8%.
  • Day-partitioned fetches (the cache flow) produce the identical fee set as a single wide fetch on both chains — no boundary loss, no duplicates.
  • 48/48 window-edge assertions pass using real actions' own timestamps as inclusive window edges on both chains.

Follow-ups (inherited from the shared tracker pattern, not introduced here)

  • offset pagination isn't insert-stable for the recent window (could dedupe on txID or use nextPageToken)
  • non-contiguous LRU cache misses can double-count across the fetch span (shared pattern in thorchain/zrx)
  • metadata.swap.inPriceUSD/outPriceUSD could eventually replace the depths call with exact per-swap pricing

🤖 Generated with Claude Code

…iate indexer

The unchained affiliate fee indexer underreported drastically (124 fees
all-time vs midgard's 379): it required the affiliate outbound and swap
event in the same block (missing streaming swaps), its history was capped
by the node's block-search index, and websocket gaps had no backfill.

Fetch ssmaya actions from midgard instead, paging descending under the
timestamp param only — mayachain midgard's fromTimestamp expects
nanoseconds (unlike thorchain's seconds) and pages unstably when combined
with timestamp, so the window's lower bound is enforced client-side.

Price fees at historical CACAO spot derived from the USDC pool depth
history (assetPriceUSD / assetPrice), replacing current-price enrichment —
consistent with the thorchain tracker's /history/rune approach.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
revenue-dashboard Ready Ready Preview Jul 31, 2026 8:48pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kaladinlight, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 41 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b57c4be4-ab0b-4e62-8d8c-913d8532c035

📥 Commits

Reviewing files that changed from the base of the PR and between 9fe147c and 0af4a37.

📒 Files selected for processing (4)
  • apps/revenue-api/src/affiliateRevenue/mayachain/constants.ts
  • apps/revenue-api/src/affiliateRevenue/mayachain/mayachain.ts
  • apps/revenue-api/src/affiliateRevenue/thorchain/constants.ts
  • apps/revenue-api/src/affiliateRevenue/thorchain/thorchain.ts
📝 Walkthrough

Walkthrough

Mayachain fee retrieval now uses Midgard actions and depth history. The implementation paginates affiliate actions, derives CACAO USD prices from the USDC pool, and returns transformed fee records with USD values.

Changes

Mayachain Midgard fee retrieval

Layer / File(s) Summary
Midgard contracts and configuration
apps/revenue-api/src/affiliateRevenue/mayachain/constants.ts, apps/revenue-api/src/affiliateRevenue/mayachain/types.ts
Midgard constants and typed models replace the legacy fee response and API constants.
Price and affiliate action retrieval
apps/revenue-api/src/affiliateRevenue/mayachain/mayachain.ts
The implementation retrieves CACAO prices from USDC pool depth history and fetches paginated affiliate actions with retries and time filtering.
Fee transformation and retrieval integration
apps/revenue-api/src/affiliateRevenue/mayachain/mayachain.ts
Qualifying actions become Fees records with CACAO and USD values. Cached and recent retrieval paths use Midgard directly.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant FeeService
  participant MidgardDepthHistory
  participant MidgardActions
  FeeService->>MidgardDepthHistory: Load USDC pool depth history
  MidgardDepthHistory-->>FeeService: Return CACAO USD prices
  FeeService->>MidgardActions: Load paginated ssmaya affiliate actions
  MidgardActions-->>FeeService: Return filtered actions
  FeeService->>FeeService: Transform actions into Fees records
Loading

Poem

I’m a rabbit with Midgard in sight,
Fetching CACAO prices just right.
Actions hop page by page,
Fees take their final stage,
USD joins the burrow tonight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: rewriting the MayaChain tracker to use Midgard instead of the Unchained affiliate indexer.
✨ 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 mayachain-midgard

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.

… bound

Midgard's timestamp param cuts at exactly endTimestamp*1e9 nanoseconds, so
fees landing in the last second of a cacheable day were excluded and cached
as absent. Pass endTimestamp+1 and trim to the window client-side, matching
the mayachain tracker. Verified live on both midgards: timestamp=S excludes
an action at S.xxx, timestamp=S+1 includes it, fromTimestamp is inclusive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/revenue-api/src/affiliateRevenue/mayachain/mayachain.ts (1)

61-92: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add an iteration safety cap to the pagination loop.

The while (true) loop in fetchMidgardActions has no maximum bound on iterations. It relies entirely on the API returning a partial batch (batch.length < MIDGARD_PAGE_LIMIT) or an old-enough action to terminate. If Midgard ever returns a full page repeatedly without surfacing an action older than startTimestamp (for example due to an API-side pagination bug or an unexpectedly large affiliate action count), the loop runs indefinitely inside a request handler, tying up resources.

Add a maximum page count (or a maximum offset) as a defensive bound, and log/throw when it is exceeded.

♻️ Proposed safety cap
+const MAX_MIDGARD_PAGES = 1000 // defensive cap; ~50k actions at MIDGARD_PAGE_LIMIT=50
+
 const fetchMidgardActions = async (startTimestamp: number, endTimestamp: number): Promise<MidgardAction[]> => {
   const actions: MidgardAction[] = []

   let offset = 0
-  while (true) {
+  for (let page = 0; page < MAX_MIDGARD_PAGES; page++) {
     const { data } = await withRetry('mayachain/midgard-actions', () =>
       axios.get<MidgardActionsResponse>(`${MIDGARD_BASE_URL}/actions`, {
         params: {
           affiliate: MIDGARD_AFFILIATE,
           timestamp: endTimestamp + 1,
           limit: MIDGARD_PAGE_LIMIT,
           offset,
         },
       })
     )

     const batch = data.actions ?? []
     actions.push(...batch)

     if (batch.length < MIDGARD_PAGE_LIMIT) break

     const oldest = batch[batch.length - 1]
     if (actionTimestamp(oldest) < startTimestamp) break

     offset += MIDGARD_PAGE_LIMIT
   }
🤖 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-api/src/affiliateRevenue/mayachain/mayachain.ts` around lines 61
- 92, Update fetchMidgardActions to enforce a maximum pagination page count or
offset in addition to the existing termination checks. When the cap is exceeded,
log an appropriate diagnostic or throw an error, then prevent further requests
while preserving the existing filtering and normal pagination behavior.
🤖 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/affiliateRevenue/mayachain/mayachain.ts`:
- Around line 103-114: Update the affiliate payout reduction around affiliateOut
and cacaoAmount to require affiliateOut.coins[0].asset to match MAYA.CACAO
before applying CACAO precision, pricing, and asset serialization; skip
non-CACAO payouts unless they are handled using their own asset-specific fields.

---

Nitpick comments:
In `@apps/revenue-api/src/affiliateRevenue/mayachain/mayachain.ts`:
- Around line 61-92: Update fetchMidgardActions to enforce a maximum pagination
page count or offset in addition to the existing termination checks. When the
cap is exceeded, log an appropriate diagnostic or throw an error, then prevent
further requests while preserving the existing filtering and normal pagination
behavior.
🪄 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 Plus

Run ID: 1017d65f-142b-4c5d-8e16-a7feae619468

📥 Commits

Reviewing files that changed from the base of the PR and between bb3cd9c and 9fe147c.

📒 Files selected for processing (3)
  • apps/revenue-api/src/affiliateRevenue/mayachain/constants.ts
  • apps/revenue-api/src/affiliateRevenue/mayachain/mayachain.ts
  • apps/revenue-api/src/affiliateRevenue/mayachain/types.ts

Comment thread apps/revenue-api/src/affiliateRevenue/mayachain/mayachain.ts
…O pricing

The fee record hardcodes CACAO decimals (1e10), the CACAO spot price, and
the CACAO assetId, so a non-CACAO affiliate out would be serialized as a
mispriced CACAO fee. Skip (and warn on) any non-CACAO payout instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pricing

Same latent issue as the mayachain tracker: the fee record hardcodes RUNE
decimals (1e8), the RUNE spot price, and the RUNE assetId, so a non-RUNE
affiliate out would be serialized as a mispriced RUNE fee. Skip (and warn
on) any non-RUNE payout instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kaladinlight
kaladinlight merged commit 7986f86 into main Aug 3, 2026
4 checks passed
@kaladinlight
kaladinlight deleted the mayachain-midgard branch August 3, 2026 16:50
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.

1 participant