Consolidate reports-server backlog: partner fixes, test suite + CI, Swapter/NYM/Revolut plugins, v2 dashboard - #232
Open
j0ntz wants to merge 24 commits into
Open
Consolidate reports-server backlog: partner fixes, test suite + CI, Swapter/NYM/Revolut plugins, v2 dashboard#232j0ntz wants to merge 24 commits into
j0ntz wants to merge 24 commits into
Conversation
Co-authored-by: Cursor <cursoragent@cursor.com>
When an asset has a contract address it is a token, but several plugins silently fell back to a native (tokenId: null) mapping when the token could not be resolved. That prices the token with the chain's gas-token rate and overcounts volume whenever the token is worth less than the gas token. - nexchange: throw when a contract-bearing asset is on a chain whose tokenType is missing, instead of returning tokenId: null. - changenow: drop the try/catch around createTokenId that swallowed failures and returned tokenId: null. - rango: drop the per-tx try/catch that logged and continued, silently dropping any transaction whose asset could not be resolved. All three plugins paginate oldest-to-newest and persist progress on throw, so a failing order halts and is retried next run rather than being mispriced or silently dropped. Also add the SUI and MONAD chain mappings to rango: these were previously dropped silently and would now halt the plugin. Verified by reprocessing the last three months of orders (nexchange 22.7k, changenow 61.9k, rango 2.9k) with zero processing failures. Co-authored-by: Cursor <cursoragent@cursor.com>
Some older LetsExchange transactions return null network fields for unambiguous native assets (e.g. ETH, BTC, XRP), causing processing to throw "Missing network" and stalling the query. Add a currency-to-network fallback for 1:1 native tickers so these transactions process correctly. Co-authored-by: Cursor <cursoragent@cursor.com>
These mappings were causing the query engine to halt and stop scanning forward for the affected partners (fail-closed design), blocking collection of newer transactions until the mapping was added. - SideShift: map SWARMS on Solana to its mint address (delisted from the SideShift coins API, so added to DELISTED_COINS). - Rango: map the SONIC blockchain to the `sonic` Edge pluginId. - Banxa: map the "Primer Paypal Pay" and "Primer Google Pay" payment types to paypal and googlepay respectively. Co-authored-by: Cursor <cursoragent@cursor.com>
The currency cache was built from the `currencies?active=true` endpoint. When ChangeNow deactivates an asset (e.g. DASH), it disappears from the active list while historical transactions still reference it. The lookup then misses and the plugin halts fail-closed, stalling all ChangeNow transaction collection. Fetch the full currency list (omit `active=true`) so previously-listed assets continue to resolve for historical transactions. Co-authored-by: Cursor <cursoragent@cursor.com>
Banxa removed ZEC from the v2 crypto catalog, so historical ZEC orders abort the partner query and stall ingestion past July 8. Co-authored-by: Cursor <cursoragent@cursor.com>
Not private enough and is scrapable. Co-authored-by: Cursor <cursoragent@cursor.com>
Unblocks Banxa query progress stuck since Jul 24 on unrecognized KLARNA Checkout payment methods. Co-authored-by: Cursor <cursoragent@cursor.com>
j0ntz
marked this pull request as ready for review
August 10, 2026 22:21
j0ntz
force-pushed
the
jon/reports-backlog
branch
from
August 10, 2026 22:21
42d05c3 to
1912adf
Compare
This was referenced Aug 10, 2026
Closed
The suite imported three helpers from ../lib/util, a gitignored build artifact that only exists after a build, so a clean checkout could not run a single test. They live in src/demo/clientUtil, so point at the source module instead.
No job ran the tests, which is how the suite stayed broken. This one generates the gitignored clientConfig.json first, then runs mocha on every pull request.
A cleaner failure needs enough to find the record and see how its shape drifted, which is its identifier plus the field names it arrived with. Serializing the whole payload instead copies counterparty addresses and transaction ids into centralized logs, where anyone with log access can recover them; the record itself stays retrievable from Couch by that id. Partners disagree on what the id field is called, so the helper tries the common spellings in order and falls back to the field list alone.
Two additions to StandardTx and the buckets built from it. Revenue: some partner APIs report Edge's actual fee per order, and estimating that same number from a rate discards a fact already in hand. revenueUsd holds the figure and revenueSource records how it was obtained, so a consumer can tell a reported number from a derived one. It is stored at ingest as a fact about the order and never recomputed; the cache engine sums it into the buckets. Orders without a reported fee are left for the dashboard to estimate at read time, so correcting a rate fixes history immediately. Chained pair keys: pair totals were keyed by currency code alone, which merges assets that share a ticker across chains. The cache now dual-writes the chained key alongside the plain one, so existing readers keep working while a reader that understands chains can separate them. checkUpdateTx gains both revenue fields. It compares an explicit field list, and revenue arrives late by nature, so without them a re-poll that filled in a fee without touching another tracked field looked unchanged and the write was skipped.
j0ntz
force-pushed
the
jon/reports-backlog
branch
from
August 19, 2026 23:50
13ca789 to
f803df0
Compare
j0ntz
force-pushed
the
jon/reports-backlog
branch
from
August 19, 2026 23:59
f803df0 to
4450ece
Compare
Queries Swapter's tool-history endpoint and maps each order to a StandardTx. Pages are buffered so a mid-page failure retries idempotently, the upper time bound is frozen before the walk so paging cannot drift, and progress only advances once the full walk completes.
Queries NYM's partner reporting endpoint and converts its native-unit amounts to major units using the live currencies list, falling back to a built-in decimals table when that fetch fails. The report timestamp keys off createdDate, because completedDate is null even on settled orders. Chain pluginIds are recorded per order so pair keys can distinguish assets that share a ticker across chains.
Queries Revolut Ramp's orders endpoint with the X-API-KEY header. Three shapes of that API each break a naive port: start and end are date-only, the response is a bare array paged by skip and limit rather than a cursor envelope, and order ids are not unique because Revolut returns one row per payment attempt. Since orderId keys the StandardTx document, attempts are collapsed to one winner per id, settled beating unsettled, before anything is emitted. Revolut reports its own partner fee in USD, so a settled order carries it as reported revenue. Native assets resolve to their Edge chain with a null tokenId. Tokens resolve their chain but leave tokenId undefined on purpose: Revolut reports no contract address, and minting one from a guess would mis-price the asset.
Two defects that only surface against live data. The nullable response fields used asOptional, which supplies its fallback for a missing key but still throws when the key is present with an unexpected type. One odd row therefore aborts the whole page, exhausts the retries, and stalls the window, so the same row is re-hit every cycle and the plugin never progresses. A zero contract address denotes the chain's native gas asset, but only a missing or empty address was treated as native, so createTokenId would mint a tokenId for the gas asset and mis-route its rates and volume.
Wires Swapter, NYM and Revolut into the query engine and the demo partner list, alongside the nexchange entry already present.
j0ntz
force-pushed
the
jon/reports-backlog
branch
from
August 20, 2026 00:12
4450ece to
ad95f0f
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit ad95f0f. Configure here.
ChangeNow, Rango and Xgram each let a per-transaction failure escape the processing loop. That looks like the safe choice, but it stalls the partner outright: the run halts, progress saves just short of the bad row, and every later poll re-fetches the same range and dies there, so nothing newer is ever recorded. Xgram was worse again, because its loop sat outside the try/catch guarding the fetch, so the throw discarded every order already processed in that run. Emitting the row anyway is not the alternative either, since that prices a token with the chain's gas-token rate. So the row is quarantined: dropped, never emitted with wrong data, and reported with its id and a per-run count. Ingestion continues past it. Shared failures stay shared, so ChangeNow loads its currency cache once outside the guard, where an outage still aborts the run rather than being mistaken for a bad row. LetsExchange had the mirror defect. It treated an order predating the API's network fields and a recent order it simply could not resolve as the same thing, so the second was saved with chain and token data silently undefined. The two are now distinguished and the unresolvable one is reported, with guidance that splits single-chain tickers from multi-chain ones. Every loop also runs under a page cap, so a stuck cursor or a page that never shortens can no longer leave the run length as the partner's decision.
Both plugins recorded currency codes without the chain they settled on, so pair keys could not tell apart assets that share a ticker. Each order now carries its resolved chain pluginId, which is what the chained pair keys in the analytics cache consume.
A redesigned dashboard served at /v2/, fully separate from the v1 demo: its own entry point, its own parcel bundle, its own URL. No v1 route, component or build output changes. Where behaviour overlaps, code is duplicated into v2 rather than refactoring anything v1 depends on. It reads the real /v1 reporting API, plus an isolated /v2/config route for per-provider rev-share rates and fiat/swap classification. Rates live on the app doc in reports_apps beside each partner's credentials, never in source, because they are commercial terms and this repo is public. Revenue is a first-class metric rather than gross volume alone. Where a partner reports Edge's actual fee the dashboard uses that figure directly and marks it; everywhere else it estimates volume times the rate at read time, so a corrected rate fixes history at once while reported figures stay immutable. Dates are handled in UTC throughout, matching the buckets the API returns, and a seeded apiKey is stripped from the address bar once read so it does not persist in history or a copied link. A chain filter and a syncProdCache helper round it out.
nexchange, Simplex and Bridgeless have no working reporting plugin, and none of them is blocked on work this repo can do alone. Record what each one needs, with the live request and exact response behind every claim, so the investigation is not repeated. nexchange is credential-scoped: its key authenticates but returns 403 on the reporting resource. Simplex needs a server-side key only their support issues, and the shipped plugin also targets a retired host with the wrong auth header. Bridgeless needs no key at all, but its public API carries no timestamp, no referral filter and no resumable cursor, which is what makes a plugin infeasible rather than merely expensive.
j0ntz
force-pushed
the
jon/reports-backlog
branch
from
August 20, 2026 00:20
ad95f0f to
8fba1f1
Compare
Recognize MoonPay Cash App payments and payouts, and normalize Banxa Checkout card payouts so both query cursors can advance.
Normalize Banxa DLocal PIX payout labels so the query cursor can continue through newer sell orders.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

CHANGELOG
Does this branch warrant an entry to the CHANGELOG?
Dependencies
noneDescription
Consolidates every open strand of edge-reports-server work into one branch, per
the umbrella task
reports-server: Umbrella.
jon/reports-backlogis what has been running ondeploy, so this is largely arequest to make
mastermatch what production already serves, plus the fixesfound while assembling it.
Supersedes #228,
#230 and
#231, all closed in
favour of this one, and folds in the chain-filter work that was sitting on a
separate branch with no PR. Nothing was lost in either move: the branch was
verified file by file against each of them first.
The nine commits below ours are partner fixes by other authors, carried
unchanged with their original SHAs.
What is in it
Test suite and CI (
Fix broken mocha test suite,Add CI job to run the test suite):test/util.test.tsimported from../lib/util, a gitignored buildartifact, so a clean checkout could not run a single test. Repointed at the
source module, and a CI job added so the suite cannot silently break again.
Analytics cache (
Carry reported revenue and chained pair keys through the analytics cache):StandardTxgainsrevenueUsdandrevenueSource, so apartner-reported fee is stored as a fact at ingest and a consumer can tell a
reported figure from a derived one. Pair totals now also dual-write a chained
key, so assets that share a ticker across chains stop merging, while existing
readers keep working.
Three reporting plugins (
Add Swapter...,Add NYM Swap...,Add Revolut...): each driven against the live partner API before shipping. Revoluthad been recorded as credential-blocked; it was pointed at a host and path that
do not exist, and the real Ramp API answers with the key already in
env.json.Its orders endpoint returns one row per payment attempt, so attempts are
collapsed per
orderIdbefore anything is emitted.Partner robustness (
Harden nexchange...,Quarantine unprocessable partner transactions...,Store chain pluginIds...): an unprocessable row used toescape the processing loop in ChangeNow, Rango and Xgram, which stalls the
partner outright, since the run halts and every later poll re-fetches the same
range and dies on the same row. It is now quarantined, dropped and reported with
its id while ingestion continues, and never emitted with wrong pricing.
LetsExchange had the mirror defect and silently dropped chain data; it now
reports the gap, and Kado no longer returns a truncated batch that reads as a
successful update.
Every partner loop also runs under a per-run page budget, so a stuck cursor
cannot leave the run length as the partner's decision. Xgram walks newest to
oldest, where a watermark cannot express an unfinished walk, so it persists the
page it stopped on and resumes there rather than restarting and never reaching
older history.
v2 dashboard (
Add isolated v2 reports dashboard): a redesigned dashboard at/v2/, with v1 untouched. Revenue is a first-class metric: reported figures areused directly and marked, everything else is estimated at read time from a
per-partner rate on the app doc, so a corrected rate fixes history at once. Rates
are never committed, since they are commercial terms and this repo is public.
Blocked-provider record (
Document the blocked partner reporting APIs): whatnexchange, Simplex and Bridgeless each need before they can work, with the live
request and exact response behind every claim. Tracked at
its own task.
Testing
verify-repo.sh --base origin/masterpasses end to end: CHANGELOG, install,prepare, eslint over every changed file, and the mocha suite at 119 passing.
The three reporting plugins were each driven against the live partner API when
they were written, read-only with the GUI's existing credentials and never
against production CouchDB: Revolut 1,162 real orders (1,359 raw rows collapsed,
0 duplicate
orderIds), NYM 47, Swapter 14, every emitted row validated againstthe repo's own
asStandardTxwith zero failures.Not verified here: nexchange cannot be exercised at all until a reporting-scoped
key exists, so its fixes are covered by the type checker, eslint and the suite
rather than by live data. The reasoning is in the commit and in
docs/blocked-partner-reporting.md.