your choice is remembered. visit/?choose=1to switch.
# contact.md
# Contact
- email: `mandloideep22@gmail.com` (fastest path)
- github: [@mandloideep](https://github.com/mandloideep)
- linkedin: [thedeepmandloi](https://linkedin.com/in/thedeepmandloi)
- resume: [/resume.pdf](/resume.pdf)
## What roles
New-grad SWE, AI engineering, and full-stack roles where the agent
isn't theatre. Currently teaching CS300 at NEIU part-time, so a
start date in the next few months works cleanly. Open to remote,
Chicago-based, or hybrid.
## What he can ship
- full-stack web apps (Spring Boot / FastAPI / Express on the back,
React on the front)
- RAG pipelines that go past the demo (eval harness, vendor portability,
per-IP caps)
- self-hosted infra on a single VPS (Docker, GitHub Actions, Dokploy,
Traefik)
- ML services that are honest about their dataset size
---
# experience.md
# Experience
## CS Instructor, NEIU (Jan 2026 to present)
Teaching CS300 (Web Development) to undergraduate cohorts at
Northeastern Illinois University. Hybrid format. Deep writes the lab
guides, the setup instructions, the assessment rubrics, and spends a
non-trivial slice of office hours helping students get Node and Git
working on their machines. Mac, Windows, and Linux all in one
classroom.
The win he likes to talk about: teaching the same idea to four
students with four different mental models in twenty minutes, without
giving them four different answers.
## AI Engineer / Research Assistant, NEIU · ACOSUS (Apr 2024 to present)
This is the research focus. ACOSUS (Advisor-Centered Outcomes Support
Unified System) is a student-success platform at NEIU that surveys
students, predicts who's likely to disengage, and gives advisors a
dashboard to act on it before the student drops. Deep is the only
engineer.
Three pieces, one researcher's hands:
- **Backend.** Node + Express + Mongo, 73.4k LOC of TypeScript, 327
REST endpoints across two API versions. Serves students, advisors,
and admins out of one app. 157 commits over 23 months.
- **Frontend.** React 18 + Vite + TanStack Query + shadcn/ui, 102k
LOC across 110 lazy-loaded routes, three role-aware shells. 151 of
155 commits are Deep's.
- **Model.** Flask + scikit-learn, 4.7k LOC. Started as a TensorFlow
neural net; became a KNN regressor when the cohort proved too small
for stable NN training. PWRS (priority-weighted response scoring)
calibration sits on top.
Specifics worth naming: he migrated a 7,000-line JavaScript codebase
into a 130,000-line TypeScript platform with role-based dynamic
surveys; cut frontend load times by ~50% with lazy loading + RTK
Query; took data-retrieval times down ~34% with schema and
aggregation work; wired GitHub Actions + Docker CI/CD that cut
release cycles ~40%. The KNN pipeline has interpretability that
neighbour-distribution confidence intervals give and a tiny dataset
can't take away.
Live at `acosus.neiu.edu`. Still shipping.
## Open Source Contributor, Dokploy (Aug 2025 to present)
Dokploy is an open-source alternative to commercial deployment
solutions. Deep developed full-stack enhancements for it, including a
profile-picture upload system and API/CLI metadata customization. He
also resolved an SSH connection bug in IP-address parsing logic and
shipped user-profile improvements that strengthened deployment
reliability and platform usability.
## Peer Mentor, CodePath (May 2025 to Dec 2025)
Mentored first-time CodePath students through semester-long 1:1
check-ins focused on study strategies, platform tools, and community
resources. The number worth quoting: 83% of his mentees reported
increased confidence in completing the course.
## What's not on the resume yet
Big-company internships. Deep is open about it. The current line is
"open to new-grad SWE / AI engineering roles." Email is fastest.
---
# facts/crazy-facts.md
# Crazy facts about Deep
- qualified for the Call of Duty Mobile indian regionals in 2020. mobile,
not pc — and he'll defend that distinction
- will argue, unprompted, that mobile games beat pc and console
- types on a kinesis split keyboard and quietly judges your QWERTY slab
- chai > coffee, taco bell > every other fast food. neither is up for debate
- can cook most indian dishes from memory, no recipe open
- reads manga and manhwa, watches anime, and maintains a backlog he swears
he'll finish
- editor hierarchy is fixed and final: claude > vim > zed > vs code
- plays chess well enough to actually mean it
---
# me.md
# About Deep
Deep Mandloi finished his MS in Computer Science at Northeastern
Illinois University in Chicago in December 2025. He builds full-stack
web apps and small agent things, mostly out of his apartment in
Chicago, mostly on one Hetzner box.
His main day-job right now is teaching CS300 (Web Development) at NEIU
as a CS instructor — writing lab guides, debugging student dev
environments across Mac, Windows, and Linux, and grading rubrics that
actually mean something.
The rest of his time goes into ACOSUS, a student-success research
platform at NEIU where he's the sole engineer across a TypeScript /
Express / Mongo backend, a 102k-line React frontend, and a Flask
KNN model service. He's been on it since April 2024. It's live at
acosus.neiu.edu and the work underpins an in-progress DSI paper.
Outside of that he ships side projects on a single VPS — CommentDraw
(a YouTube giveaway picker with Stripe billing), Atelier (a RAG
chatbot for academic papers), and a small-scale n8n-style workflow
builder he wrote to learn how visual automation tools work under the
hood. He also contributes full-stack to Dokploy, an open-source
deployment platform.
He picks boring tools: Postgres, Redis, FastAPI, React, Docker on a
VPS. He cares more about ship-frequency and observable systems than
about fashionable stacks. Open to new-grad SWE and AI-engineering
roles — remote, Chicago-based, hybrid, doesn't matter much.
---
# projects/agent-portfolio.md
# deepmandloi.com
## Overview
This site. A three-mode portfolio: visitors land on a chooser, then
go to a polished browser bento at `/`, a chat surface at `/chat`, or
a Claude-code-style terminal at `/terminal`. All three wrap the same
small agent that answers questions about Deep using the markdown
corpus you're reading right now.
The three surfaces share theme tokens, density steps, and the same
OpenRouter pipe, so picking Solarized Light in the terminal carries
to the portfolio and the chat. Same content, three different reading
postures.
## Challenges
Three front doors out of one repo. The mode chooser at `/` writes
`localStorage["portfolio.mode"]` and skips itself on return visits.
That sounds trivial until you try to ship SSR + a localStorage
read-on-mount + a redirect without flashing the wrong UI. The fix
was a tiny client-only redirect island that runs after hydration.
SSR serves the chooser; hydration decides whether to bounce to `/`,
`/chat`, or `/terminal`.
Theme tokens as a typed registry. Each theme is a `{ slug, name,
vibe, tokens }` object in `src/content/themes.ts`; CSS variables get
emitted at SSR by `generateThemeCss`. Adding a theme means adding one
object, no edits to `styles.css`, no edits to component files.
`tailwind-merge` didn't know about the custom `--text-*` and
`--spacing-*` tokens, so `cn()` calls were silently dropping
`text-eyebrow` next to `text-muted/80` and rendering chips at the
wrong size. The fix was `extendTailwindMerge` with explicit class
groups for every custom token.
Content split. Structured data (status enums, tags, slugs) lives in
`src/content/site.ts`. Prose lives in `src/content/agent/**.md`. A
`pnpm check-content` script asserts the two stay in sync; if a
project's slug appears in `site.ts` without a markdown file, the
script exits non-zero in CI.
## Learnings
No telemetry theatre. Activity lines in the terminal show real server
steps (`loading theme registry`, `mounting command registry`,
`warming agent context`), not fake spinners. When something is
genuinely waiting (the OpenRouter handshake), the line says so.
One primitive per chrome shape. The chrome bar across portfolio,
chat, and terminal is six different controls (density toggle, theme
switcher, model picker, command hint, mode switcher, info chip).
Without a shared `<ChromeButton>` primitive every author picked a
slightly different radius / height / opacity, and the result felt
like six different controls because it *was* six different controls.
Consolidating into one component with explicit `size` and `tone`
variants, plus a `<ChromeButtonGroup>` for the segmented density
control, fixed the family resemblance.
## Stack
TanStack Start (React 19) with file-based routing under
`src/routes/`. TanStack Query for cache, TanStack Form for the chat
input, TanStack Store for the agent + theme + density stores.
Tailwind v4 with the design tokens in `@theme inline`; shadcn/ui +
Magic UI components installed through the shadcn CLI. The agent
talks directly to OpenRouter over `fetch` + SSE, no adapter library.
Drizzle ORM against Postgres for the rate-limit table (Neon
serverless in prod, Docker Postgres in dev). Biome for lint +
format, Vitest for tests, Playwright for the visual passes.
## Status
WIP. 34 commits in, 590 tests passing in CI, 7 themes registered,
and counting.
---
# projects/atelier.md
# Atelier
## Overview
Atelier is a RAG chatbot for academic papers. Deep built it so a
student or researcher can drop in a PDF, a URL, or an arXiv ID, get
the doc chunked and embedded into an isolated Qdrant collection, and
then ask questions that get answered from the paper instead of from
the model's priors.
It started as a workshop project to ship LangGraph + DeepEval
end-to-end rather than read about them. The version that's live today
routes every query through a classifier first (retrieve-from-paper,
verify-a-claim against the web and arXiv, or just answer directly),
so a question like "is this finding still current?" runs a different
graph than "what's the methodology?"
## Challenges
The `langchain-google-genai` 4.2.3 client had a broken
`embed_documents` when called with a batch: silent wrong-shape
returns. Deep wrapped it in a small `_PerQueryEmbeddings` adapter in
the LLM factory that re-routes batches to single calls. Ugly, but the
rest of the RAG pipeline didn't have to know.
DeepEval's default judge text-parses JSON out of the model response.
With Gemini that was flaky, so Deep wrote a custom judge in
`backend/eval_judge.py` that uses Gemini's `responseSchema` to force
structured output. The first eval run then hit Gemini's free-tier
rate limit immediately. 5 metrics × goldens × parallel workers was
too much, so the runner got capped at 3 workers with a 5-second
throttle.
The bigger shift was the last two commits. Turning a localhost demo
into something that could sit on the open internet meant adding a
SQLite-backed per-IP daily cap, a per-session message cap, file-size
and chunk limits on ingest, and an `APP_OFFLINE=1` env flag that
disables the LLM calls but leaves the read-only UI working. None of
it was interesting code, but without it the Gemini key would have
been drained by the first crawler.
## Learnings
LangGraph earns its abstraction tax once you have more than two
branches. The router + relevancy check + single rewrite gave bounded
retry behavior — at most 3 retrieval attempts and 1 query rewrite,
without the infinite loops you get from naive agentic patterns.
Vendor portability is mostly an embedding-cache problem, not an
LLM-call problem. The factory lets you flip `LLM_PROVIDER=openai` and
the chat path just works, but the Qdrant collections were embedded
with one model's vectors and don't move with you. Next time Deep
would keep the embedding choice and the chat choice as separate
decisions from day one.
Evaluating RAG is mostly about measurement, not about the model.
DeepEval's contextual precision + recall + faithfulness scores caught
a class of confident-but-unsupported answers that eyeballing the chat
would have missed. The eval pipeline is now the thing he'd build
first on the next RAG project, not last.
## Stack
Python 3.14, Streamlit for the UI, LangGraph for the workflow,
Qdrant Cloud for vectors with LangChain's `CacheBackedEmbeddings`
writing to local disk so restarts don't re-embed. Gemini 2.5-flash by
default (free tier), OpenAI as a fallback through a small LLM
factory. DeepEval for the test pipeline with a custom Gemini judge,
Tavily for web search, SQLite checkpointer for per-session history.
Docker image on GHCR, Dokploy on a 4GB Hetzner VPS behind Traefik and
Let's Encrypt.
## What's next
Still running. On the list: a wider eval set than the single
research-report PDF, cite-as-you-answer inline citations in the UI,
and an async ingest worker so a 200-page PDF doesn't block the chat
thread while it embeds.
---
# projects/commentdraw.md
# CommentDraw
## Overview
CommentDraw picks giveaway winners from YouTube comments. A creator
pastes one or more video URLs, the backend pages through the YouTube
Data API, dedupes commenters by channel ID, optionally filters by
keyword, and runs a loyalty-weighted draw that favors people who
showed up across multiple videos. One click, one winner, no scrolling
through three thousand comments at 1am.
It's aimed at small-to-mid creators who run giveaways and don't want
to do the bookkeeping by hand. Three plans (free, gold, diamond) with
quotas enforced server-side before any external call goes out. Stripe
handles billing in test mode; the rest of the stack is
production-shaped.
## Challenges
The Stripe SDK was the loudest one. Between minor versions,
`Event.getObject()` started returning empty optionals for webhook
payloads because internal field names drifted. Deep switched the
webhook handler to `deserializeUnsafe()` against the raw JSON, which
sidesteps the type-mapping mismatch entirely. It's the documented
escape hatch but easy to miss. The failure mode is silent, just
signed events that quietly parse to nothing.
Rate limiting was the second one. A single-instance token bucket
falls apart the moment you horizontally scale, and a public
YouTube-API wrapper is exactly the kind of thing that gets hammered.
Deep wired Bucket4j to a Redis (DragonflyDB in prod) proxy manager so
the buckets live in shared state, keyed per email per endpoint, with
daily resets via date-based cache keys. Quota enforcement also runs
at the service layer *before* the upstream call, so a misbehaving
client can't burn the YouTube quota by hitting an alternate endpoint.
The third was free-tier infra fighting back. Aiven's free MySQL tier
auto-shuts-down on inactivity, which is fine until you wake up to a
cold backend at 3am. Fix: a scheduled `@Scheduled` job that pings the
db daily at 9 UTC. Ugly but it works, and the alternative was paying
for a tier Deep didn't need.
## Learnings
Spring's `ApplicationEventPublisher` carried more weight than
expected. Payment success, registration, password reset, account
deletion — all five became events with their own listeners, and the
controllers stopped knowing about email at all. The Brevo client only
exists in one place. Swapping to SES later would be a one-file
change.
On the frontend, RTK Query's `baseQueryWithReAuth` middleware turned
out to be the move. The rest of the app never sees a 401: the
middleware intercepts, hits the refresh endpoint, retries the
original request, and the components stay unaware. That pattern saved
a lot of error-handling boilerplate Deep had already written twice on
previous projects.
If he did it again he'd skip Aiven and just run MySQL on the same
VPS. The keep-alive cron, the TLS config, the network round-trip,
none of it was worth the savings. The rest of the architecture held
up.
## Stack
Java 25 on Spring Boot 3.5 for the backend, with Spring Data JPA,
Spring Security + JWT, Bucket4j for rate limiting, and the Stripe and
YouTube Java clients for the external calls. React 19 + Vite 7 on
the frontend, Redux Toolkit + RTK Query for state and api, Tailwind 4
for styling, Framer Motion + Lottie + canvas-confetti for the winner
reveal. MySQL 8.4 on Aiven, DragonflyDB for cache and rate-limit
state, Cloudinary for avatars, Brevo for transactional mail. GitHub
Actions builds an image to GHCR, Dokploy on a Hetzner 4GB VPS pulls
it, Traefik does the TLS. Cloudflare Pages serves the frontend.
## What's next
Instagram and TikTok comment ingestion are the obvious extensions:
same selection algorithm, different sources. Multi-creator team
accounts would unlock the higher tier. And at some point the
test-mode Stripe keys need to come out, but that's a launch decision,
not an engineering one.
---
# projects/workflow-builder.md
# Workflow Builder
## Overview
A small-scale, learning-focused take on n8n. Deep built it to
understand how visual workflow tools actually work under the hood,
not to compete with n8n. The shape is the same (a canvas where you
drop integration nodes, wire them together, fire the chain by hitting
a public webhook), but the scope is deliberately narrow: three node
types, one user, one box.
The point was to ship every layer end-to-end: a React Flow canvas
with proper graph state, a FastAPI backend with a topological-sort
DAG executor, real refresh-token rotation, credentials encrypted at
rest. It works, it runs on one Hetzner host, and it taught him most
of the things he'd want to know before tackling a real automation
platform.
The frontend is 6,979 lines of TypeScript across 79 components: React
18, ReactFlow 11 for the graph, Zustand for state, shadcn/ui on top
of Radix and Tailwind. The backend is 1,343 lines of Python: FastAPI,
SQLAlchemy 2.0 async, Pydantic 2, a small topological-sort executor
that walks the graph and awaits each node handler. One repo, one
Docker image, one box.
## Challenges
The async database migration was the first real wall. The project
started on sync SQLAlchemy + SQLite. Moving to Postgres meant
rewriting every query path through `AsyncSession`, but the painful
part was Alembic. Running migrations on startup inside FastAPI's
lifespan looks fine until you realize Alembic's `command.upgrade()`
calls `asyncio.run()` internally, which throws when there's already a
running event loop. The fix was wrapping the upgrade call in
`asyncio.to_thread()` so migrations run on a worker thread and the
loop stays free. One stray blocking call elsewhere would have
silently strangled concurrency without raising anything; async
hygiene is brittle in a way that linters don't catch.
Refresh-token rotation was the second one. Naive rotation is easy;
rotation that survives two browser tabs refreshing at the same time
is not. Deep designed a `refresh_tokens` table tracking `jti`,
`family_id`, `used_at`, and `replaced_by`, with a 10-second grace
window: if a token gets used twice within 10 seconds it's a
concurrent tab, not theft, and a new pair is issued. Past 10 seconds,
the whole family is revoked and the user is forced to sign in again.
Signin itself runs a dummy argon2 hash on unknown emails so "wrong
password" and "no such user" both take ~100ms, defeating the timing
oracle. The whole flow is maybe 80 lines but every one of them is
load-bearing.
The third was credentials at rest. Third-party secrets like Gmail app
passwords, Telegram bot tokens, and Slack webhook URLs were sitting
in a plaintext JSON column. Deep wrote a 30-line `db/encryption.py`
wrapping `cryptography.Fernet`: serialize, encrypt, base64, store.
The executor decrypts on load before dispatching to handlers. The
trade-off is intentional. Rotating `CREDENTIALS_ENCRYPTION_KEY`
invalidates every stored credential, which forces real key-rotation
discipline instead of pretending keys are forever.
## Learnings
Biggest takeaway: async Python is one missed `await` away from
quietly serializing everything. Nothing crashes, latency just creeps
up. Deep now treats every new dependency as suspect until he's
checked whether it has an async variant or runs through
`asyncio.to_thread`.
Second: security primitives are short but unforgiving. The
refresh-token logic is the smallest file in the routers folder and
the one he rewrote the most times. The 10-second grace window came
from actually testing it in two Safari windows and watching the
family revoke fire on a legitimate refresh. Constant-time signin came
from reading a writeup about a timing attack on a different framework
and realizing he'd built the same hole.
Third: self-hosting on a single 4GB Hetzner box via Dokploy + GHCR is
a much better dev loop than he expected. Manual deploy via
`workflow_dispatch` (no auto-deploy on push) sounds clunky and is
actually a feature, because every deploy is a deliberate act and
rollback is "redeploy the previous tag." He'd default to this stack
for every personal project going forward.
## Stack
Python 3.11 with FastAPI 0.136 and SQLAlchemy 2.0 on the async
driver, Postgres + Alembic for the data layer, JWT via python-jose
with argon2-cffi for passwords and Fernet for credential encryption.
React 18 + TypeScript + Vite on the frontend, ReactFlow 11 for the
canvas, Zustand for state, TanStack Query for server cache,
shadcn/ui + Tailwind for everything visible. python-telegram-bot,
SMTP for Gmail, plain webhooks for Slack. Multi-stage Dockerfile
(Node 24 Alpine to Python 3.14-slim), two uvicorn workers, GHCR image
pushed by a manual GitHub Action, Dokploy webhook for redeploy.
## What's next
This stays a learning project, not a product. The realistic next
steps are the ones that round out the lessons: a websocket execution
trace so the canvas lights up live as a workflow runs, a real test
suite (the security audit doc flags it as the biggest hole and he
agrees), and scheduled triggers (cron nodes) so workflows can fire
without an inbound webhook. New node types come if and when he needs
them for his own glue work.
---
# skills.md
# Skills
## Languages
TypeScript and Python are the daily drivers. Java for the Spring Boot
side of CommentDraw. JavaScript when a project still has unmigrated
files. SQL when the ORM is in the way. Bash for one-off operations.
## Backend
Spring Boot 3.5 on Java 25 for the SaaS surface. FastAPI on Python
for everything else; Workflow Builder runs on FastAPI + SQLAlchemy
2.0 async. Express on Node for the ACOSUS backend. Flask for the ML
service. Pydantic 2 for schemas; Zod when the schema is on the
TypeScript side.
## Frontend
React 19 with Vite. TanStack Query is the default for server cache;
Redux Toolkit + RTK Query when the API surface gets dense (CommentDraw,
ACOSUS). shadcn/ui on top of Radix + Tailwind 4. React Flow for any
graph-shaped UI. Lexical for rich-text. Recharts for dashboards.
## Data
Postgres is the default. Mongo when the schema actually is
document-shaped (ACOSUS). MySQL on Aiven for CommentDraw because the
free tier was good enough until it wasn't. Redis / DragonflyDB for
caches, queues, and distributed rate-limit state. Qdrant for vectors
in Atelier. SQLAlchemy 2.0 async, Mongoose, and JPA on the ORM side.
Deep has shipped against all three.
## AI
LangGraph + LangChain for the multi-branch RAG pipeline in Atelier.
scikit-learn for the production KNN in ACOSUS; TensorFlow lives in
the archived path. DeepEval for the eval pipeline (contextual
precision / recall / faithfulness scored at a 0.7 threshold). Gemini
2.5-flash as the default LLM (free tier), with OpenAI as the
fallback through a small factory. OpenRouter for the agent on this
site. Direct HTTP + SSE for streaming; no adapter library.
## Infra
Docker for everything that goes to prod. GitHub Actions for CI/CD,
typically a manual `workflow_dispatch` build to GHCR to Dokploy
webhook redeploy. Hetzner 4GB VPS for the side projects. Cloudflare
in front; Traefik terminating TLS via Let's Encrypt. Multi-stage
Dockerfiles to keep the runtime image small.
## Games
Call of Duty Mobile — qualified for the Indian regionals in 2020.
Mobile-first by conviction; he'll argue mobile beats PC and console.
Plays chess well enough to mean it.
## Fun
Anime, plus a manga and manhwa backlog he swears he'll finish. Cooks
most Indian dishes from memory, no recipe open. Types on a Kinesis
split keyboard. Chai over coffee, Taco Bell over every other fast
food — neither up for debate.