An AI agent that researches the web in a think → search → read → think again loop, holds conversation context, and generates downloadable PDF reports — with bring-your-own-key (OpenAI, Anthropic, Kimi, or any OpenAI-compatible endpoint) and usage-based cost tracking.
Built for the DrDroid Product Engineer assignment.
┌── Next.js (Vercel) ──┐ ┌── FastAPI (Railway) ──────────────┐
│ social login │ SSE │ ReAct agent loop │
│ paywall │◄──────►│ billing (Stripe + coupon) │
│ chat + timeline │ REST │ BYOK key vault (Fernet) │
│ usage dashboard │ │ usage ledger + cost engine │
└──────────┬───────────┘ └──────┬─────────────┬──────────────┘
│ supabase-js │ asyncpg │ user's LLM key
▼ ▼ ▼
Supabase Auth Supabase Postgres Brave / Stripe / LLM
(Google + GitHub) + Storage (PDFs)
- Social login only (Google + GitHub) via Supabase Auth.
- Paywall after signup — unlock with coupon
SID_DRDROIDor a real $5 Stripe card payment. Either grants 5 credits. - Agentic chat: streams the agent's activity timeline (searching, reading pages, generating PDF), holds context per thread, supports multiple chats, stop button.
- PDF artifacts: the agent writes styled reports and attaches them as downloadable files.
- Bring your own key: OpenAI-compatible key + endpoint. Presets for OpenAI / Anthropic / Kimi, or any custom base URL. Prompt caching enabled; cached tokens tracked.
- Usage & cost dashboard: per-chat cost, split by input / output / cache-read / cache-write tokens, priced by the exact model selected.
micromanus/
├── backend/ FastAPI service (routes → controllers → services → repositories)
├── frontend/ Next.js 15 App Router (Tailwind + Supabase + SWR + Zustand)
└── supabase/migrations SQL schema, functions, RLS, seed pricing, storage bucket
- Create a project at supabase.com (free tier).
- SQL Editor → run each file in
supabase/migrations/in order (001→005). - Authentication → Providers → enable Google and GitHub.
- Add redirect URL:
https://YOUR-FRONTEND.vercel.app/auth/callback(andhttp://localhost:3000/auth/callbackfor local dev). - Google: create OAuth credentials in Google Cloud Console; GitHub: create an OAuth App.
- Add redirect URL:
- Storage → confirm the private
artifactsbucket exists (migration005creates it). - Grab from Project Settings → API: Project URL,
anonkey,service_rolekey, and the JWT Secret (or leaveSUPABASE_JWT_SECRETempty to use JWKS). - Connect → Transaction pooler → copy the
DATABASE_URL.
cd backend
cp .env.example .env # fill in every value
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" # ENCRYPTION_KEYDeploy: push to a repo, create a Railway service from the backend/ directory (it auto-detects the Dockerfile), set all env vars from .env.example, deploy. Health check: GET /health.
Brave Search key: free tier at api-dashboard.search.brave.com (2,000 queries/mo).
- Dashboard in test mode → copy
STRIPE_SECRET_KEY(sk_test_…). - Developers → Webhooks → Add endpoint:
https://YOUR-BACKEND.up.railway.app/webhooks/stripe, eventcheckout.session.completed. Copy the signing secret →STRIPE_WEBHOOK_SECRET. - Test card:
4242 4242 4242 4242, any future expiry, any CVC.
The success page also polls
/billing/confirm, which verifies the session directly against Stripe — so credits land even if the webhook is delayed.
cd frontend
cp .env.example .env.local # NEXT_PUBLIC_SUPABASE_URL / ANON_KEY / API_URL
npm install && npm run devDeploy: import frontend/ into Vercel, set the three NEXT_PUBLIC_* env vars (API_URL = your Railway URL), deploy. Then set the backend's FRONTEND_ORIGIN to the Vercel URL and redeploy the backend (CORS + Stripe redirect URLs).
Seeded in supabase/migrations/004_seed_pricing.sql and served from /models. Costs are computed
per LLM call from this table — verify prices against provider pages before launch and update rows
as new flagship models ship.
- User LLM keys encrypted at rest (Fernet); only the last 4 chars ever reach the browser.
- RLS on every table; all privileged writes go through the backend service role.
- Coupon redemption is one-per-user and rate-limited; credit spend is a single atomic SQL statement (race-safe); webhook + confirm are idempotent on Stripe IDs.
- Fetched web content is wrapped in
<untrusted_content>and the agent is instructed not to follow instructions inside it (prompt-injection hygiene).fetch_urlrefuses private/loopback IPs (SSRF guard).
# terminal 1
cd backend && python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000
# terminal 2
cd frontend && npm install && npm run dev # http://localhost:3000