Per-user monthly token quotas tracked in DynamoDB; tier field added to user profiles

dropthejase adds DynamoDB-backed per-user credits, a 429 gate in the API, and a tier field surfaced in the account settings sidebar. The credits modal was added and then removed in the same cluster - the enforcement path existed but the UI was never wired end-to-end.

multi-tenantinfrastructure

A CreditsTable DynamoDB table (PAY_PER_REQUEST billing) is provisioned in CDK. The agent tracks usage via a Strands AfterModelCallEvent hook. The initial implementation increments a call counter (credits_used :one), but 5af54ad corrects this to accumulate totalTokens from the event's stop data instead. Calls are nearly free; tokens are what cost money. That change from call-count to token-count is the useful detail here - it's the right metering primitive.

The credits modal (CreditsExhaustedModal) is added and rendered in three chat pages, but 5969976 removes it after noting it was never wired. The props (creditsExhausted, creditsResetDate, dismissCreditsModal) are stripped from ChatView and useAssistantChat in the same commit. The enforcement path at the API level (429 on quota exceeded) remains, but the frontend signal to the user is gone. This is a feature that's half-done.

The tier column (text not null default 'Free') is added to user_profiles schema, the GET /user/profile response includes it, and the AppSidebar dropdown now displays "Free Tier" or "Pro Tier" based on the DB value. A content-type header fix (7ccf115) ensures profile PUT requests are parsed correctly, and a stopPropagation call fixes a sidebar dropdown toggle bug found while working in the same area.

So what Worth a look for the token-metering pattern specifically: the `AfterModelCallEvent` hook accumulating `totalTokens` into a DynamoDB atomic ADD is a clean, low-overhead approach. Skip the credits enforcement code in its current state - the modal was removed and the 429 path has no working UI counterpart. The tier display is cosmetic until something actually sets `tier` to a non-default value.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

5 commits from dropthejase/louis, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
ce16fcd1 Add per-user monthly credits tracking (DynamoDB, 429 enforcement, frontend modal) Jason Lee 2026-05-08 ↗ GitHub
commit body
- CDK: add CreditsTable (DynamoDB PAY_PER_REQUEST) with lambdaRole + agentCoreRole grants, CREDITS_TABLE_NAME env var on Lambda, and CreditsTableName/Arn outputs
- Backend: new credits.ts lib with checkCredits/incrementCredits using @aws-sdk/client-dynamodb; wire 429 credits_exhausted guard at top of POST /chat and POST /projects/:id/chat handlers
- Frontend: detect 429 credits_exhausted in useAssistantChat streamChat error path; surface CreditsExhaustedModal (already existed) with resetDate in ChatView, assistant page, chat/[id] page, and project assistant chat page
5969976c chore: remove dead credits enforcement UI (never wired) Jason Lee 2026-05-09 ↗ GitHub
5af54ade fix(agents): track totalTokens in credits_used instead of call count Jason Lee 2026-05-11 ↗ GitHub
7265ee79 feat: expose and display tier from user_profiles Jason Lee 2026-05-12 ↗ GitHub
Add tier column to user_profiles schema (default 'Free').
GET /user/profile now returns tier; UserProfileContext maps it.
AppSidebar and AccountPage display as '${tier} Tier' instead of hardcoded 'Free'.
7ccf1157 fix: account settings dropdown nav, profile PUT content-type, tier display Jason Lee 2026-05-12 ↗ GitHub
commit body
- stopPropagation on sidebar dropdown toggle and Account Settings button
  so outside-click listener doesn't race and swallow the navigation
- Add Content-Type: application/json to all three profile PUT calls in
  UserProfileContext - body was arriving as Buffer, UPDATE never executed
- Rename 'Models & API Keys' tab to 'Models' (no API keys with Bedrock)
- Add request + error logging middleware to Express app for CloudWatch

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-339.md from inside the repo you want the changes in.

⬇ Download capture-thread-339.md