Skip to content

Fix KYB org members being sent to identity verification on Rewards - #1952

Merged
efstajas merged 1 commit into
mainfrom
fix/rewards-checkpoint-kyb-exemption
Aug 17, 2026
Merged

Fix KYB org members being sent to identity verification on Rewards#1952
efstajas merged 1 commit into
mainfrom
fix/rewards-checkpoint-kyb-exemption

Conversation

@efstajas

@efstajas efstajas commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

The problem

A member of an org with a KYB record opens /wave/rewards, and instead of their rewards they land on /wave/kyc-required β€” apparently being asked to verify their identity a second time, with no way through.

Why

rewards/+layout.ts guarded the whole section on GET /api/liveness-checkpoints/status:

const checkpoint = await getLivenessCheckpointStatus(fetch, 'grant_access');
if (!checkpoint.satisfied) throw redirect(302, `/wave/checkpoint?backTo=…`);

That endpoint reports whether the caller has passed a check. It does not report whether they need one β€” it evaluates the user alone and never looks at a grant. Only the API can answer that question, and only per grant: grants belonging to an org with a KYB record are exempt, because their withdrawers are authorized by the company record rather than by a personal identity check.

So the guard redirected exempt members away before any grants endpoint was ever called. The API would have served their grants perfectly happily. The layout's own comment asserted the premise that made this look safe β€” "The API refuses the grants endpoints outright in that case" β€” which is only true for grants that aren't exempt.

It then compounded: checkpoint/+layout.ts sees an unsatisfied checkpoint, checks for personal KYC, and finds none β€” which is correct and expected for a KYB withdrawer β€” so it forwards them to /wave/kyc-required. Hence the dead end, and hence the "why do I need to KYC again?".

rewards/+page.svelte was already KYB-aware and would have rendered correctly. It just never got the chance.

The fix

Stop pre-gating; let the API's refusal do the routing. The grants endpoints already respond with liveness_checkpoint_required, and call.ts already turns that into LivenessCheckpointRequiredError β€” nothing was catching it. Its own doc comment already described the intended handling.

  • Delete rewards/+layout.ts.
  • Add handleCheckpointRequired(err, backTo) to lib/utils/wave/liveness.ts β€” redirects into the challenge flow for that one error, re-throws everything else.
  • Catch it in rewards/+page.ts and rewards/[grantId]/+page.ts, which also inherit the depends('wave:liveness-checkpoint') the layout carried.
  • Catch it in openWithdrawalFlow, for a check falling due between page load and click.

Net βˆ’2 lines. Three things improve as a side effect:

  • One fewer round trip on the rewards page. The page load awaits parent(), so the status call was serialized ahead of the grants fetch rather than running alongside it.
  • The single-grant route gets exact. The API decides that one per grant, instead of a user-level pre-gate standing in for it.
  • The kyc-required dead end resolves itself, since /wave/checkpoint is now only reached after a genuine refusal.

No backend change. /api/liveness-checkpoints/status stays what it is β€” the checkpoint flow itself asks exactly that question. It was only ever wrong as a stand-in for "do I need one".

Testing

npm run check β€” 0 errors, 177 warnings, unchanged from before. eslint and prettier --check clean on the touched files.

Not covered by an automated test: the gating logic itself is tested on the backend, but this lived entirely in the frontend route guard, which is how it got through.

Intended behaviour, called out to save a reviewer the detour

A user holding both a personal grant and an exempt org grant is gated on the whole list, org grant included. That is correct and not something this PR should change: the personal grant requires personal KYC on its own terms, and an approved KYC satisfies the checkpoint directly through the recent_kyc path, so there is no additional step and no dead end. The org grant is simply gated alongside for that window.

The bug this PR fixes is specifically the case where every grant is exempt, and the user was gated anyway.

Follow-up, not in this PR

  • Navigating directly to /wave/checkpoint as a KYB withdrawer still dead-ends at kyc-required. Nothing routes there anymore, so it takes a deliberate URL entry.

The rewards route guarded itself on GET /api/liveness-checkpoints/status,
which reports whether the caller has passed a check β€” not whether they
need one. Only the API can answer that, and only per grant: grants
belonging to an org with a KYB record are exempt, since their withdrawers
are authorized by the company record rather than by a personal identity
check.

The guard therefore redirected exempt members away before any grants
endpoint was called. Because those members have no personal KYC by
design, the checkpoint flow then bounced them on to /wave/kyc-required,
leaving them unable to reach Rewards at all and looking at an apparent
demand to verify their identity a second time.

Drop the pre-gate and react to the API's refusal instead. The grants
endpoints already respond with liveness_checkpoint_required, and call.ts
already translates that into LivenessCheckpointRequiredError; nothing was
catching it. Handling it at the three call sites also removes a round
trip from the page load, which awaited parent() and so serialized behind
the status call, and makes the single-grant route exact, since the API
decides that one per grant rather than per user.
@efstajas
efstajas merged commit 14b1ad4 into main Aug 17, 2026
6 of 8 checks passed
@efstajas
efstajas deleted the fix/rewards-checkpoint-kyb-exemption branch August 17, 2026 08:12
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