Skip to content

Rate limiting for mutating API routes, real OrderEntry balances, and TradingEngine persistence - #158

Merged
akargi merged 6 commits into
Chulilee:mainfrom
akargi:feature/api-rate-limiting-order-balances-engine-persistence
Nov 5, 2026
Merged

akargi merged 6 commits into
Chulilee:mainfrom
akargi:feature/api-rate-limiting-order-balances-engine-persistence

Conversation

@akargi

@akargi akargi commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes three open issues on the trading stack:

1. API layer rate limiting (closes #60)

The trading engine enforced rate limits internally, but the API layer accepted unlimited requests before they ever reached it. This adds a fixed-window API rate limiter (src/lib/api-rate-limit.ts) that mirrors the engine's windowMs/maxRequests pattern:

  • Per-endpoint limits with sensible defaults (orders/payments tighter, analytics looser) and a configurable default tier.
  • Configurable programmatically via setApiRateLimitConfig() and via environment variables (API_RATE_LIMIT_DEFAULT, API_RATE_LIMIT_ORDERS_CREATE, … — see .env.example).
  • Buckets keyed by endpoint + caller (authenticated client id, falling back to x-forwarded-for / x-real-ip, then anonymous).
  • Expired buckets are swept periodically; Retry-After and X-RateLimit-* headers are set on rejection.
  • Applied to all mutating endpoints: POST /api/v1/orders, POST /api/v1/orders/batch, DELETE /api/v1/orders/[id], POST /api/v1/payments, POST /api/v1/payments/webhook, POST /api/v1/analytics/events, POST /api/v1/admin/disputes, DELETE /api/v1/admin/listings, PATCH /api/v1/admin/users.
  • 429 responses carry a Retry-After header.

2. OrderEntry real balances (closes #61)

The percentage buttons (25/50/75/100%) used a hardcoded mock balance (10,000 USD / 1 BTC). Now:

  • OrderEntry receives quoteBalance / baseBalance props (plus optional quoteCurrency / baseCurrency labels).
  • Percentage sizing uses the real available balances; unusable/zero balances are treated as zero.
  • The available balance for the active side is displayed in the UI (available-balance test id).
  • TradingDashboard wires the balances from the portfolio service (getHoldings: USD value of stablecoin holdings as quote), debits/credits them as orders are placed, and passes them into OrderEntry.

3. TradingEngine persistence (closes #62)

All engine state was in-memory only, so the order book and trade history were lost on restart. Now:

  • New persistence contract (src/lib/trading-persistence.ts) with FileTradingPersistence (atomic JSON snapshot via temp-file + rename) and InMemoryTradingPersistence backends.
  • Backend is configurable via TRADING_PERSISTENCE (file | memory | none) and TRADING_STATE_PATH, or programmatically through the TradingEngine constructor; defaults to file in the app singleton (getTradingEngine) and none under NODE_ENV=test so unit tests stay hermetic.
  • Every mutating path (submit, batch submit, cancel) snapshots orders, fills and trades. On startup the engine restores orders, fills, the trade tape and the per-client order index, and rebuilds resting books from open/partial_fill limit orders — so the order book survives a restart.
  • The default snapshot file (.trading-engine-state.json) is git-ignored.

Test coverage

New unit tests cover the rate limiter (limits, per-endpoint/client buckets, 429 + Retry-After), engine persistence (book/orders/trade-tape restore across a simulated restart, file backend round-trip, env selection) and the OrderEntry balance props (display, real-balance sizing, zero-balance behavior). CI was not run locally per the author''s workflow; tests run in CI.

Closes #60, closes #61, closes #62

Add a fixed-window API rate limiter (src/lib/api-rate-limit.ts) that runs before any mutating route handler. It mirrors the trading engine's windowMs/maxRequests pattern, keys buckets per endpoint and per caller (authenticated client id, falling back to forwarded/real IP), sweeps expired entries periodically, and returns 429 with a Retry-After header plus X-RateLimit-* headers when the limit is exceeded.

Limits are configurable per endpoint programmatically and via API_RATE_LIMIT_<ENDPOINT> environment variables (see .env.example), with a default tier for unlisted endpoints.

Applied to all mutating API endpoints: POST /api/v1/orders, POST /api/v1/orders/batch, DELETE /api/v1/orders/[id], POST /api/v1/payments, POST /api/v1/payments/webhook, POST /api/v1/analytics/events, POST /api/v1/admin/disputes, DELETE /api/v1/admin/listings and PATCH /api/v1/admin/users.

Closes Chulilee#60
OrderEntry no longer falls back to a hardcoded 10000 USD / 1 BTC mock. The actual available account balance arrives via props (quoteBalance / baseBalance, with optional quoteCurrency / baseCurrency labels), percentage shortcuts size against those balances, and the component shows the available balance for the active side in the UI.

TradingDashboard wires the balances from the portfolio service (getHoldings) - the USD value of stablecoin holdings as the quote balance, debited/credited as orders are placed - and passes them into OrderEntry.

Closes Chulilee#61
…starts

Add a persistence layer for the TradingEngine: a small synchronous TradingPersistence contract plus FileTradingPersistence (atomic JSON snapshot on disk) and InMemoryTradingPersistence backends, selected via TRADING_PERSISTENCE (file|memory|none) and TRADING_STATE_PATH or programmatically through the TradingEngine constructor.

Every mutating path (order submit, batch submit, cancel) snapshots the full engine state. On startup the engine restores orders, fills, the trade tape, the per-client order index, and rebuilds the resting order books from open/partial_fill limit orders, so the book survives a restart with no events emitted during recovery.

The app singleton (getTradingEngine) restores from the configured backend; under NODE_ENV=test persistence defaults to none so unit tests stay hermetic and direct construction remains in-memory. The default snapshot file is git-ignored.

Closes Chulilee#62
Add TRADING_PERSISTENCE / TRADING_STATE_PATH and the API_RATE_LIMIT_* override variables to .env.example.

Refs Chulilee#60, Chulilee#62
@vercel

vercel Bot commented Nov 5, 2026

Copy link
Copy Markdown

@akargi is attempting to deploy a commit to the Akargi Team on Vercel.

A member of the Team first needs to authorize it.

@vercel

vercel Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
inter-changabletrade-fricks Ready Ready Preview Nov 5, 2026 7:17am UTC

createTradingPersistenceFromEnv now accepts a plain string map so callers do not need to cast partial env objects to NodeJS.ProcessEnv.
next/server requires the global Request, which jsdom does not provide; the route-handler suites already use the node environment docblock.
@akargi
akargi merged commit 62f1d39 into Chulilee:main Nov 5, 2026
3 checks passed

This branch was successfully deployed

1 active deployment
Preview — cc81c6df Deployed Nov 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant