Skip to content

Cursor pagination, buy rate limiting, body validation, and volume leaderboard - #789

Merged
Chucks1093 merged 1 commit into
accesslayerorg:mainfrom
JamesVictor-O:feature/issues-778-779-780-785
Aug 26, 2026
Merged

Cursor pagination, buy rate limiting, body validation, and volume leaderboard#789
Chucks1093 merged 1 commit into
accesslayerorg:mainfrom
JamesVictor-O:feature/issues-778-779-780-785

Conversation

@JamesVictor-O

Copy link
Copy Markdown
Contributor

Summary

Resolves four backend hardening/discovery issues assigned in this wave:

  • Add cursor-based pagination to the key holders list endpoint #778 — Cursor-based pagination on the key holders endpoint. GET /api/v1/creators/:id/holders now accepts cursor alongside the existing limit/offset. When cursor is supplied, the endpoint switches to keyset pagination on the (ownerAddress, creatorId) unique index and returns a signed, tamper-checked nextCursor (reusing the existing encodeCursor/decodeCursor utility already used by the creator feed). Existing offset-mode behavior and response shape are unchanged.

  • Add rate limiting middleware to the key purchase endpoint to prevent rapid-fire buy requests #779 — Rate limiting on the key purchase endpoint. Added a per-wallet sliding-window limiter (src/middlewares/wallet-rate-limit.middleware.ts), 5 requests / 10s, backed by a Redis sorted set (ZADD/ZREMRANGEBYSCORE/ZCARD) so the window slides continuously rather than resetting on a fixed boundary. Exceeding the limit returns 429 with a Retry-After header and logs the breach (wallet + timestamp). An x-internal-service-key header matching the optional INTERNAL_SERVICE_KEY env var bypasses the limit for internal calls. Fails open (allows the request) if Redis is unreachable, so a cache outage can't block purchases.

  • Add input sanitization and schema validation middleware to all request bodies using Zod #780 — Centralized Zod body validation. Added validateBody(schema) (src/middlewares/validate-body.middleware.ts): runs schema.safeParse(req.body), strips unknown fields (Zod's default z.object() behavior), and returns a structured 422 listing every invalid field before the request reaches the controller. Applied to the four real route groups in this codebase that accept a body: auth registration, key purchase (buy), post creation, and profile update.

    Note: the issue's acceptance criteria mention "key sale" as one of four groups, but this codebase has no sell/key-sale endpoint yet (only buy and multi-buy). I covered buy, post creation, profile update, and auth registration instead — the middleware is generic, so a future sell endpoint can adopt it in one line. Profile update's body-validation status code changes from 400 → 422 for consistency with the rest of the API (creator-profile-update.integration.test.ts updated accordingly — path-param validation on that route stays 400).

  • Add a leaderboard endpoint returning the top 20 creator keys ranked by total trading volume #785 — Volume leaderboard endpoint. Added GET /api/v1/creators/leaderboard/volume, returning the top 20 creator keys ranked by total trading volume (buys + sells combined) over a rolling window (default 7 days, LEADERBOARD_VOLUME_WINDOW_DAYS). Volume is aggregated from the Activity read model (amount * price_at_trade on KEY_BOUGHT/KEY_SOLD rows). Results are cached in Redis for LEADERBOARD_VOLUME_CACHE_TTL_SECONDS (default 5 minutes) and the cache is invalidated whenever the indexer records a new trade. Response fields match the issue spec: rank, keyId, creatorName, avatarUrl, totalVolume, priceChange24h (the last computed from the creator's existing price snapshot). Every Redis operation is time-bounded (1s) so a Redis outage degrades to "compute live" / "skip invalidation" instead of hanging a request or the indexer pipeline.

Test plan

  • pnpm tsc --noEmit — clean
  • pnpm eslint on all touched files — clean
  • New unit/integration tests added for all four changes (cursor pagination, rate limiter, validateBody middleware, volume leaderboard service + controller)
  • Full test suite run on this branch vs. main baseline to confirm no regressions: main has 98 failing suites / 288 failing tests (pre-existing, mostly DB-integration tests that need a live Postgres instance unavailable in this sandbox); this branch has 93 failing / 287 failing with 25 more tests total — no new failures introduced
  • Fixed a hang risk found while testing: indexer-pipeline.integration.test.ts didn't mock Redis and would hang once the pipeline started invalidating the leaderboard cache — added a Redis mock there, matching the pattern already used by subscription.integration.test.ts

Closes #778
Closes #779
Closes #780
Closes #785

…me leaderboard

Resolves four backend hardening/discovery issues:

- Add cursor-based (keyset) pagination to GET /creators/:id/holders
  alongside the existing offset mode, using the wallet address as the
  cursor via the existing signed-cursor utility.
- Add a per-wallet sliding-window rate limiter (5 req / 10s, Redis-backed)
  to the key purchase endpoint, with a 429 + Retry-After response, breach
  logging, and an internal-service bypass header.
- Add a centralized validateBody Zod middleware and apply it to the auth
  register, key purchase, post creation, and profile update routes so
  invalid bodies are rejected with a structured 422 before reaching
  business logic and unknown fields are stripped.
- Add GET /creators/leaderboard/volume returning the top 20 creator keys
  by total trading volume over a rolling window, cached in Redis (5 min
  TTL) and invalidated whenever the indexer records a new trade.

Closes accesslayerorg#778
Closes accesslayerorg#779
Closes accesslayerorg#780
Closes accesslayerorg#785
@drips-wave

drips-wave Bot commented Aug 25, 2026

Copy link
Copy Markdown

@JamesVictor-O Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Chucks1093
Chucks1093 merged commit 3e1d30d into accesslayerorg:main Aug 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment