Skip to content

Consolidate reports-server backlog: partner fixes, test suite + CI, Swapter/NYM/Revolut plugins, v2 dashboard - #232

Open
j0ntz wants to merge 24 commits into
masterfrom
jon/reports-backlog
Open

Consolidate reports-server backlog: partner fixes, test suite + CI, Swapter/NYM/Revolut plugins, v2 dashboard#232
j0ntz wants to merge 24 commits into
masterfrom
jon/reports-backlog

Conversation

@j0ntz

@j0ntz j0ntz commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Consolidates every open strand of edge-reports-server work into one branch, per
the umbrella task
reports-server: Umbrella.

jon/reports-backlog is what has been running on deploy, so this is largely a
request to make master match what production already serves, plus the fixes
found 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.ts imported from ../lib/util, a gitignored build
artifact, 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): StandardTx gains revenueUsd and revenueSource, so a
partner-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. Revolut
had 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 orderId before anything is emitted.

Partner robustness (Harden nexchange..., Quarantine unprocessable partner transactions..., Store chain pluginIds...): an unprocessable row used to
escape 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 are
used 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): what
nexchange, 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/master passes 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 against
the repo's own asStandardTx with 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.

MMrj9 and others added 9 commits July 31, 2026 15:05
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>
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>
Comment thread src/demoV2/index.html
Comment thread src/queryEngine.ts
Comment thread src/demoV2/index.html
Comment thread src/demoV2/index.html
Comment thread src/partners/nym.ts Outdated
Comment thread src/partners/nexchange.ts Outdated
Comment thread src/partners/changenow.ts
Comment thread src/partners/changenow.ts
Comment thread src/partners/letsexchange.ts
j0ntz added 4 commits August 19, 2026 16:47
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
j0ntz force-pushed the jon/reports-backlog branch from 13ca789 to f803df0 Compare August 19, 2026 23:50
Comment thread src/demoV2/index.html
@j0ntz
j0ntz force-pushed the jon/reports-backlog branch from f803df0 to 4450ece Compare August 19, 2026 23:59
Comment thread src/demoV2/index.html
Comment thread src/partners/swapter.ts
Comment thread src/partners/xgram.ts
j0ntz added 2 commits August 19, 2026 17:11
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.
j0ntz added 3 commits August 19, 2026 17:11
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
j0ntz force-pushed the jon/reports-backlog branch from 4450ece to ad95f0f Compare August 20, 2026 00:12

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread src/partners/xgram.ts
Comment thread src/partners/kado.ts
j0ntz added 4 commits August 19, 2026 17:20
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
j0ntz force-pushed the jon/reports-backlog branch from ad95f0f to 8fba1f1 Compare August 20, 2026 00:20
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.
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.

3 participants