Skip to content

Non-Custodial Partner Program — aggregated partner statistics endpoints - #4528

Closed
joshuakrueger-dfx wants to merge 1 commit into
developfrom
feat/partner-statistics
Closed

Non-Custodial Partner Program — aggregated partner statistics endpoints#4528
joshuakrueger-dfx wants to merge 1 commit into
developfrom
feat/partner-statistics

Conversation

@joshuakrueger-dfx

@joshuakrueger-dfx joshuakrueger-dfx commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Why

Wallet partners integrating DFX have no way to see how their integration performs. The only
partner-facing transaction endpoint returns per-transaction detail — more than a usage dashboard
needs, and more than a partner should hold. This is the interface half of the Non-Custodial Partner
Program; #4508 is the lock that must ship first.

Generic by design: there is no partner-specific code anywhere. Any wallet gets its own numbers
through the same route.

What

GET /v1/statistic/partner and GET /v1/statistic/partner/timeline, scoped to the wallet in the
company JWT. There is deliberately no walletId parameter — a partner cannot express a request
for someone else's data. Both return aggregates only: volume and transaction counts by direction,
active and new users, breakdowns by asset, fiat currency, blockchain and payment method, plus the
referral position in EUR.

Anonymity is the load-bearing part

Aggregates alone are not anonymous. Three independent reviews demonstrated, with executed output,
that a suppressed value could be recovered by subtraction — from period totals and from both
funnels, because a visible total was emitted alongside its parts.

What this PR does about it:

  • Block suppression. If any member of an additive group falls below the threshold, the whole
    group is suppressed — members, total and derived rates. No more per-cell nulling.
  • Timeline thresholds per direction, not on the bucket sum. A bucket of {buy: 5, sell: 1}
    previously passed and disclosed the single sell in the clear.
  • Periods snap to UTC day boundaries with a minimum span of one day, so two overlapping windows
    differing by hours can no longer isolate a single transaction by subtraction.
  • The threshold binds to distinct users as well as transactions. Five trades by one person used
    to pass every gate.
  • Query budget per wallet, not per IP range. The previous guard bucketed by IP, bypassed known
    and Azure addresses, counted in-memory per instance, and was off entirely unless
    REQUEST_LIMIT_CHECK was set.

Verified by executed output rather than argument: with sell below the threshold,
total − buy − swap yields null, not the hidden value. Same for both funnels.

Correctness fixes found in review

  • referral.creditOpen used partnerRefCredit − paidRefCredit. paidRefCredit covers both pots;
    the three sibling sites use refCredit + partnerRefCredit − paidRefCredit. The old formula
    understated the figure and could go negative.
  • Timeline bucketing mixed UTC period snapping with local date-part keys. Off UTC this shifted every
    bucket by a day. Truncation is now bound to UTC in SQL and the bucket keys follow.
  • map.set on bucket keys silently dropped a bucket on collision; it accumulates now.
  • Date ranges are half-open, so consecutive windows no longer double-count a boundary transaction.

Verification

  • Suite in four shards: 333 suites, 6113 tests, 0 failures. lint empty, format:check clean,
    tsc --noEmit clean.
  • Statistic specs run green under four process timezones — UTC, Europe/Zurich, Pacific/Auckland,
    America/Los_Angeles. Before the fix, Europe/Zurich produced 4 buckets where 3 were expected.
  • Mutation probes, not just deletion probes. Two of them previously proved the tests measured
    nothing: neutralising mergeNamedRows failed 0 of 32, and dropping the wallet scope from a
    single query failed 0 of 18 because the scope assertion used some instead of every. Both
    are red now, alongside probes for block suppression, day snapping, the distinct-user threshold,
    the creditOpen formula and the per-direction timeline threshold.
  • A real-Postgres integration spec now covers the breakdown pipeline, the UNION query and timeline
    buckets against actual rows, following the repo's existing MIGRATION_TEST_PG convention.

For reviewers

  • TZ is not pinned anywhere in the repo — not in the Dockerfile, compose files or workflows. This
    module no longer depends on it, but two existing modules build bucket keys from local date parts
    on the assumption that "the app and DB both run UTC". Worth a separate look.
  • One call fans out to concurrent queries; they are grouped so a single partner request cannot
    saturate the connection pool.
  • Suppression protects against reconstruction within a response and across day-aligned windows. It
    is not a formal query-budget guarantee.

Wallet partners integrating DFX have no way to see how their integration
performs. The only partner-facing transaction endpoint returns per-transaction
detail including customer IBANs, which is both more than a usage dashboard
needs and more than a partner should hold.

Add GET /statistic/partner and /statistic/partner/timeline, scoped to the
wallet in the company JWT. There is deliberately no walletId parameter, so a
partner cannot express a request for someone else's data. Both endpoints
return aggregates only: volume and transaction counts by direction, active and
new users, breakdowns by asset, fiat currency, blockchain and payment method,
and the referral position in EUR.

Aggregates alone are not anonymous. Counts below a threshold of five are
suppressed; the effective count is min(transactions, distinct users). Additive
groups (period totals by direction, timeline buckets) use block suppression —
if any member is under the threshold the whole group is null — so a hidden
value cannot be recovered via totals − visible. Period bounds snap to UTC day
boundaries with a half-open interval and a one-day minimum. Referral open
credit uses refCredit + partnerRefCredit − paidRefCredit on the wallet owner
account; tradingUsers and referral balances are gated by the same threshold.
Rate limits for these routes count by wallet id, not IP prefix.
@joshuakrueger-dfx
joshuakrueger-dfx force-pushed the feat/partner-statistics branch from 06c1896 to 9bf880c Compare July 31, 2026 07:32
@joshuakrueger-dfx

Copy link
Copy Markdown
Collaborator Author

Superseded — reopened as a fresh PR on the corrected revision.

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