Most AI applications rely solely on IP-based rate limiting to prevent abuse. Unfortunately, that's only the first line of defense.
In this article, we'll build a production-inspired anonymous identity system using FastAPI, MongoDB, and Beanie that tracks visitors using fingerprints, enforces daily usage limits, prepares for future authentication, and lays the foundation for a scalable AI SaaS.
We'll also see how this integrates cleanly with a multi-provider LLM Router without polluting business logic.
π Introduction
When building AI products, most developers focus on prompts, LLMs, and fancy frontend interfaces.
Very few spend time thinking about what happens before the AI is even called.
That request has to be identified.
Validated.
Protected.
Tracked.
And eventually analyzed.
While working on CVForbes, an AI-powered resume tailoring platform, I realized that relying only on IP-based rate limiting wasn't enough.
I wanted something that could:
- Track anonymous visitors
- Limit free usage
- Detect abusive clients
- Prepare for authentication later
- Keep the API routes completely clean
So instead of sprinkling checks across endpoints, I built a dedicated identity and abuse protection layer.
π€ Why Not Just Use IP Rate Limiting?
Traditional rate limiting looks something like this:
@limiter.limit("3/minute")
That's a good start.
But users can easily bypass it by:
- Changing networks
- Using a VPN
- Switching devices
- Clearing browser state
For an AI product where every request costs money, that's not enough.
We needed another layer.
π‘ The Idea
Instead of identifying requests using only an IP address, every request first becomes a Client Identity.
Client Request
β
βΌ
Client Identity
β
βββ IP Address
βββ User-Agent
βββ Cookie
βββ Fingerprint
That fingerprint becomes the user's anonymous identity throughout the application.
π Architecture
Client Request
β
βΌ
IP Guard Middleware
β
βββ Resolve Client Identity
βββ Check Active Ban
βββ Get/Create Anonymous User
βββ Reset Daily Usage
βββ Validate Daily Limit
βββ Attach User to request.state
β
βΌ
API Endpoint
β
βΌ
CV Generation Service
β
βΌ
LLM Router
(Automatic Failover Between Providers)
β
βββββββββββββββΌββββββββββββββΌβββββββββββββ
βΌ βΌ βΌ βΌ
Groq OpenRouter Ollama Gemini
β β β β
βββββββββββββββ΄ββββββββββββββ΄βββββββββββββ
β
βΌ
Structured Output Validation
β
βΌ
Tailored CV + Cover Letter
Notice something?
The route knows absolutely nothing about users.
The middleware handles everything before the request reaches business logic.
π Organizing the Backend
Instead of putting everything inside routes, I split responsibilities.
app/
βββ middleware/
β βββ ip_guard.py
β
βββ services/
β βββ client_identity_service.py
β βββ anonymous_user_service.py
β βββ abuse_service.py
β
βββ repositories/
β βββ anonymous_user_repository.py
β βββ banned_ip_repository.py
β
βββ models/
β βββ anonymous_user.py
β βββ banned_ip.py
β βββ generated_cv.py
Each layer has only one responsibility.
π Designing the Anonymous User
Instead of storing only an IP address, each anonymous visitor stores useful analytics.
class AnonymousUser(Document):
fingerprint: str
ip: str
user_agent: str
cookie_id: str
requests_today: int = 0
total_requests: int = 0
abuse_score: int = 0
is_banned: bool = False
Now we know:
- who visited
- how often
- when they visited
- whether they were abusive
without requiring sign-up.
π§ Services Instead of Fat Routes
The route doesn't increment counters anymore.
Instead:
await anonymous_service.register_request(user)
Behind that single line:
- reset daily limit
- validate usage
- update timestamps
- increment counters
- save to MongoDB
The endpoint remains tiny.
π‘ Middleware Does the Heavy Lifting
The middleware performs the entire security pipeline.
identity = identity_service.resolve(request)
user = await anonymous_service.get_or_create(identity)
await anonymous_service.register_request(user)
request.state.user = user
Now every endpoint automatically receives a validated anonymous user.
No duplicated logic.
π« Preparing for Bans
Another service handles abuse.
if await abuse_service.is_banned(identity.fingerprint):
raise HTTPException(
status_code=403,
detail="Access denied."
)
Later this can become:
- manual admin bans
- automatic bans
- permanent bans
- temporary bans
- abuse score thresholds
without changing middleware.
π€ Integrating with the LLM Router
One of my favorite parts is that the protection layer has nothing to do with AI providers.
Whether the router selects:
- Groq
- OpenRouter
- Ollama
- Gemini
the security layer behaves exactly the same.
That separation keeps the architecture flexible.
π Current Flow
A request now follows this path:
Request
β
Identity
β
Anonymous User
β
Daily Limit
β
Ban Check
β
API
β
CV Service
β
LLM Router
β
Structured Validation
β
Response
Simple.
Predictable.
Easy to extend.
π― Final Thoughts
Building an AI application isn't only about prompts or choosing the fastest LLM. The real engineering challenge is designing a backend that remains maintainable as the product grows.
By introducing an anonymous identity layer, separating responsibilities into middleware, services, and repositories, and keeping the LLM Router completely independent, CVForbes now has a foundation that's easy to extend with authentication, subscriptions, analytics, and future AI providers.
Sometimes the best feature isn't the one users seeβit's the architecture that quietly keeps everything secure, scalable, and understandable behind the scenes.
Did you miss my deep dive into provider-agnostic AI architecture? You can find it right here:
How I built a provider-agnostic AI architecture that automatically switches between Groq, OpenRouter, Ollama, and Gemini.






Top comments (0)