Building an AI-Native Portfolio: A Case Study in Agentic Engineering
Executive summary
I rebuilt nickvirden.com from scratch — replacing a legacy Express/Gulp site with a Next.js 16 application — as both a professional portfolio and a working demonstration of how I actually build software: by directing AI coding agents through a disciplined plan-review-implement-verify loop, not by prompting a chatbot and shipping whatever comes back.
The rebuild started from a simple provocation — a portfolio built as a fleet of AI agents, agent-ready by design — and grew once I started asking what "agent-ready" should actually mean in practice. It ships three fully independent visual identities (terminal, editorial, circuit) behind a single cookie-persisted theme switcher; a security hardening pass that closed a real clickjacking gap and rebuilt rate limiting from scratch; a discoverability layer — robots.txt, sitemap, llms.txt, JSON-LD — written for both search engines and language models; a validated OpenAPI 3.1 spec; a live MCP server over Streamable HTTP; and a visual-regression and accessibility suite that caught real, ship-blocking bugs before they reached production.
None of it happened as a single pass. The build shipped as eighteen sequenced, independently reviewable pull requests, each planned, adversarially reviewed, implemented, and verified by an agent that didn't write the code it was checking — a structure I insisted on partway through so I could trust a diff without reading every line of it myself. That discipline caught real defects at every stage, from an inverted honeypot that would have blocked real visitors instead of bots to a missing MCP handshake method that only a live client connection ever surfaced.
The site is also, deliberately, the argument for itself: everything it claims about how I work — plan-first, adversarially reviewed, verified by someone other than the author — is demonstrated by the commit history that built it, not just asserted in the copy.
| PRs shipped in the rebuild | 18 — each independently planned, adversarially reviewed, implemented, and verified |
|---|---|
| Visual themes | 3 (terminal, editorial, circuit) — shipped simultaneously, not A/B-tested |
| Parallel security review angles | 4 |
| MCP tools exposed | 5, all read-only |
| About-page revision rounds | ~12 |
Context & goals
The project started from a LinkedIn post about a portfolio built entirely by a fleet of AI agents, designed to be legible to other agents, not just human visitors. I'd already been running an AI-native practice professionally for months by that point, and my own site — an old Express/Gulp build that hadn't been touched in years — was nowhere close to reflecting that. I wanted a site "worthy of being live in 2026 for someone who calls themself an AI-augmented/AI-native web developer": fast, lightweight, easy to deploy, and built the way I actually build things now, not a static résumé with a nicer font.
That framing set three constraints from the start. First, the site had to be agent-legible — not just fast for humans, but structurally readable by the crawlers and tool-calling clients that are increasingly how software gets discovered and evaluated. Second, it had to hold up under real security scrutiny, not portfolio-site security theater: I asked for a multi-angle adversarial review "like a superlative staff security engineer," because I wanted the most secure site I could reasonably have, not one that merely looked secure. Third — and this constraint arrived later, reshaping the whole project — the site itself needed to demonstrate my working process, not just describe it in prose. If I was going to claim I work by planning, adversarially reviewing, and independently verifying, the build had to actually run that way, in public, in a reviewable commit history.
One practical constraint shaped a lot of early decisions: two of my most substantial recent projects were built under a former employer and are proprietary, under active development, and legally off-limits to name or describe in detail. That ruled out the most obvious way to showcase my AI-native experience — pointing at the real thing — and forced a different approach: an unattributed skills narrative in the About page, a case study built as a public, MIT-licensed technical exercise instead, and a career-timeline entry that describes scope and impact without disclosing implementation.
Working style & process
The engineering process here isn't a side note to the project — it's the actual subject of the case study, since the site's whole premise is that it demonstrates how I work. Concretely, that process looked like this, refined over the course of the build rather than arriving fully formed:
Plan, adversarially review, synthesize, implement, verify
Every non-trivial unit of work started as a plan, not a diff. Early on, that meant one planning pass. By the security-hardening phase, I was asking for parallel, multi-persona reviews: four separate security-focused agents running concurrently, each attacking a different surface — the API and Server Action attack surface; cookies, headers, and third-party scripts; dependencies and secrets; and forward-looking hardening requirements for endpoints that didn't exist yet.
I did the same thing later for a piece of writing, not just code: when I asked for the About and Resume page content, I asked for a content-writer persona and a separate interviewing-psychologist persona to land on an interview approach before either one touched a draft. Different domains, same structural instinct — get more than one expert angle before committing to an approach, then fold the findings back into a single plan rather than picking one opinion and discarding the rest.
- Plan
- Adversarial review(multiple personas / angles)
- Synthesize findingsback into the plan
- Implement
- Verify(an agent that didn't write the code)
- Ship
- Verify: real defect found leads back to Implement
- Verify: clean ships
The loop that ran the whole build, not just the security-hardening phase — sharpened over eighteen PRs rather than designed up front.
A verifier that didn't write the code
This is the load-bearing discipline in the whole process. The first PR that shipped — the Next.js rebuild itself — went out with a plain summary of what was built. I caught myself in that same summary admitting the implementer's own self-report wasn't enough. Working independently, a nested verifier agent found:
- A git-staging bug that would have silently committed the old
middleware.tswhile omitting the real file - A PostHog call with no timeout that would have turned a slow third-party API into a full site outage
- An inverted honeypot field that blocked real form submissions instead of bots
- A hamburger button with no menu wired behind it
None of that showed up in a green test suite. From that point forward, every PR of any real size got its own independent verification pass — sometimes two, when the first surfaced enough real findings to warrant checking the fix itself.
PR-stacked review, not one giant diff
The very first PR into the rebuild landed as one large, interdependent commit — the Next.js migration, the theme pivot, and the security hardening were too entangled to split cleanly. I flagged that as a problem the moment it happened: I didn't want to review one 345-file diff — I wanted a stack of small, atomic, independently reviewable PRs on their own branches.
From PR #2 onward, that's exactly what shipped: eighteen PRs total, each scoped to one concern (content-derivation layer, REST endpoints, OpenAPI spec, discoverability, MCP server, About page, Resume page, and so on), sequenced by real dependency order, each with its own branch, its own implementer pass, and its own verifier pass.
Partway through, I made a deliberate call to let the build run autonomously through everything that didn't require a product or content decision from me — with one explicit boundary: surface anything ambiguous, fold small fixes into the PR you find them in, and file a follow-up issue rather than silently expanding scope for anything larger. That's exactly what happened with a CI flakiness bug discovered mid-build: filed as an issue, not patched in place, because chasing it down would have meant guessing at font-rendering nondeterminism between CI runner instances instead of doing the feature work in front of me.
I own my mistakes in the record, not around it
Partway through separating a bug fix from test-infrastructure work across two branches, I ran git checkout -- on four files, intending to discard an accidental carry-over — a command that destroys uncommitted work with no undo. It wiped out real, unrecovered-from-anywhere work: the Playwright configuration, a testing dependency, and documentation that had just been written.
I caught it by verifying the actual project structure in a test run rather than trusting my own memory of what should have been there, reconstructed what could be reconstructed from the parts that survived — the actual test files were untracked and unaffected — and re-verified the reconstruction end-to-end before committing anything on top of it. I'm including that here rather than glossing over it because it's the same discipline the rest of the process runs on: verify against the real state of the system, not against what you assume happened.
A dedicated interview process for content only I could supply
Code has patterns to copy and objective criteria — tests pass, types check, axe reports clean. Career narrative doesn't. For the About and Resume pages, I asked for something closer to a structured interview than a content-generation task: one open-ended prompt designed to avoid résumé-recitation mode, follow-up questions mined from what I actually said rather than a pre-built list, and — critically — genuine pushback when a draft overstated something.
When I proposed "technical and product leadership" as a description of my own work, that got challenged directly: the evidence I'd given supported technical and design ownership, not roadmap or prioritization authority, and the honest recommendation was to not make the stronger claim just because it sounded better. I agreed and cut it. The published About page went through roughly a dozen rounds of exactly this kind of back-and-forth — phrasing, claims, ordering, one clause at a time — because it's the one part of the site where "good enough" isn't a technical question, it's a question of whether it actually sounds like me.
Architecture & technical decisions
Next.js 16, App Router, next-yak, TypeScript
The legacy site was Express and Gulp — no component model, no type safety, nothing that would age well or move fast. Next.js 16's App Router gives Server Components by default, which matters a lot for a multi-theme site (more on that below), since it means a visitor's browser only ever downloads the code for the theme they're actually looking at. For styling, I passed on Tailwind v4 in favor of next-yak — zero-runtime, styled.div template-literal components that work natively in Server Components without forcing a "use client" boundary just to style something. That choice came directly out of prior experience: I'd used Emotion's styled-components pattern professionally, and I wanted the same authoring ergonomics without paying a client-side runtime tax on every themed component — a cost that matters more here than in a typical app, given there are three complete visual systems to ship.
Three real themes, not a random A/B assignment
The project went through a real pivot here worth walking through, because the reasoning behind it changed the whole shape of the site. The original plan was to build three visual directions — terminal (dark, monospace, hacker-toned), editorial (warm, serif, agency-grade), and circuit (dark slate with an SVG circuit motif built off my logo) — as competing variants in a PostHog A/B/C experiment, and let real visitor behavior pick the eventual winner.
Terminal
Editorial
CircuitI liked all three enough that I didn't want to wait for that data, or throw away two-thirds of the design work. Instead, all three ship simultaneously, permanently, behind a persistent switcher in the header — closer to the multi-brand theming problem I'd solved before (supporting distinct brand skins for HBO, Cinemax, TNT, and other properties on a shared platform) than to a conversion experiment.
A visitor without a preference gets a fixed default (terminal) rather than a random assignment, so the choice doesn't get logged as if it were a signal and so search engines see one consistent page rather than a moving target. That same consistency requirement is why the theme lives in a cookie read server-side, not in client-side state or localStorage: the server renders only the matching theme's component tree, so there's exactly one canonical page per visitor, not three competing for indexing priority.
Security hardening as a dedicated, adversarial phase, not a checklist
I asked for a security review run as if by "a superlative staff security engineer," fanned out across four parallel review angles, because I wanted every file and every API call evaluated, not just the obvious ones. The findings were real, not theoretical:
- Zero HTTP security headers existed anywhere on the site, meaning clickjacking was actually possible, not just a missing best practice
- Cookies were missing
secureandhttpOnlyattributes for no defensible reason - The analytics integration initialized itself and wrote a persistence cookie before a visitor had answered the consent banner — quietly undercutting the banner's own claim that nothing gets tracked until you opt in
Fixing that last one took two attempts: the first fix broke the ability for a returning visitor who'd already consented to re-opt-in, caught by a follow-up verification pass before it ever shipped.
The rate limiter also got rebuilt from the ground up — the original implementation had:
- A spoofable IP key
- Unbounded memory growth
- A body-size cap that trusted a spoofable header
The rebuild shares one bucket between the web contact form and the not-yet-built MCP contact tool, deliberately closing off a "hit both paths to double your rate" gap before that second path even existed.
Some risk was accepted rather than engineered away: full IP-spoofing resistance needs an edge/platform-level solution that's out of proportion to a portfolio site's actual traffic, and the CSP still allows 'unsafe-inline' for scripts because React Server Component hydration has no nonce plumbing to attach a stricter policy to yet — both documented as conscious tradeoffs, not oversights.
A discoverability layer built for both search engines and language models
This is the part of the site that most directly answers the original "agent-ready portfolio" premise. robots.txt takes a deliberate, asymmetric stance: allow every named crawler, including AI training crawlers. The reasoning: the site's whole purpose is to be read and cited by agents, all of its content is already public elsewhere, and allowing a citation crawler is fully reversible while allowing a training crawler is not — a tradeoff worth naming rather than defaulting into either direction silently.
sitemap.xml lists only real, indexable pages, deliberately excluding the machine-readable JSON/text endpoints, which are discoverable through their own channel: llms.txt, a plain-text summary of the site written for both a human skimming it and a model ingesting it, following the community llms.txt convention. JSON-LD structured data is embedded per-page, not globally, so a ProfilePage schema node describes the actual page it's attached to rather than making an inaccurate blanket claim from the root layout.
A validated OpenAPI 3.1 spec and a real MCP server, not a gesture at either
Four static JSON/text endpoints (/resume.json, /resume.txt, /projects.json, /services.json) expose the same content the site renders from, generated once at build time and served with open CORS and long cache lifetimes, since they change only on redeploy. /openapi.json is generated from the same Zod schemas those endpoints validate against, and a test strictly parses each route's live response against the schema its spec claims describes it — so if the document and the actual endpoint ever drift apart, that's a test failure, not a silent gap discovered by an integrator later. The path list is a hand-written allowlist, not a filesystem scan — a structural choice, not an oversight: it means the contact form's endpoint cannot appear in the published spec by construction, because nothing ever walks the routes directory to find it. This case study's own markdown twin is a fifth static endpoint generated from the same content module, but it's plain markdown rather than JSON, so it sits outside the Zod-validated spec above rather than adding a fifth entry to it.
The MCP server was the highest-risk, highest-effort piece of the entire rebuild, for a reason that has nothing to do with the code itself: the protocol had a breaking rewrite in mid-2026 — the session model and initialize handshake were removed entirely in favor of a stateless design — while I was actively building against it. Research surfaced that shift partway through planning, which is why the agent-facing interface leads with a documented REST/OpenAPI surface, with MCP layered on top rather than serving as the whole strategy.
Building it required correcting factual errors in my own research along the way — an error-code range that turned out to be backwards, and header enforcement that was actually strict where I'd assumed it was lenient — and, more usefully, an actual live connection from a real MCP client, which is what surfaced a required server/discover method that was missing entirely and would never have shown up in a unit test.
The finished server exposes five read-only tools over Streamable HTTP with its own rate-limit bucket, a 50,000-byte body cap, and unauthenticated access — deliberately read-only, since a write-capable tool would need a CORS and security review of its own before it could ship, and that work was consciously deferred rather than rushed into the same PR.
Testing strategy: correctness, visual truth, and accessibility as three separate guarantees
- Unit tests (Vitest) cover the theme-assignment logic, the discoverability nudge's dismissal state, the contact form's validation schema, and the content model itself.
- Playwright e2e tests exercise real user flows across all three themes: the switcher persisting across a hard refresh, the mobile popover's keyboard behavior, full contact-form submissions, and every real navigation link.
- Visual regression testing runs only in CI, against Ubuntu runners, because font rendering differs enough from local macOS development that a locally generated baseline would never match what CI produces — baselines are committed to the repo and only ever regenerated by a dedicated CI workflow, never by hand locally.
- Accessibility testing runs axe-core against every page/theme combination, with one deliberate departure from the tool's defaults: axe treats an exact 1:1 foreground/background contrast ratio as "needs human review" rather than a hard violation, on the theory that identical colors might occasionally be intentional.
There's no legitimate design where that's true for real content text, so the suite treats that specific case as blocking. That override is exactly what caught a real, ship-blocking bug: a CSS specificity conflict left every theme's primary call-to-action button hard to read, and in one theme its text was the literal same color as its own background — invisible, and something a default-configured axe run would have passed clean.
Notable tradeoffs & rejected alternatives
A/B/C testing, rejected in favor of permanent multi-theme
Shipping all three themes at once instead of A/B-testing them (see Architecture, above) meant giving up a data-driven, unambiguous "this design converts better" answer, in exchange for a feature — a persistent, switchable multi-theme site — that's more interesting and more representative of the real multi-brand work I'd actually shipped before. I think this was the right call: an A/B test would have optimized for a single metric on a portfolio site where the actual goal is closer to demonstrating range than maximizing one conversion number.
In-memory rate limiting, accepted rather than engineered around
Vercel's serverless model means an in-app rate limiter's state resets across cold starts and doesn't share state across instances — a real limitation for a portfolio site's traffic level, not a hidden one. Rather than build a distributed store (Redis, Upstash, or similar) to close that gap for a site that doesn't need it yet, the accepted mitigation is a single, free Vercel WAF dashboard rule, config over code, layered on top of the in-app limiter as defense in depth. That's a case where the more "complete" engineering solution would have been actively the wrong call — added complexity and an external dependency to solve a problem the traffic level doesn't actually create.
The theme-switcher popover, iterated through three real options before shipping
A late round of feedback flagged that logo, hamburger menu, and full theme dropdown all crammed into one mobile header row read as cluttered and non-standard. Rather than pick a fix by instinct, the response was three actual working prototypes at real viewport widths:
| Option | Trade-off |
|---|---|
Shrink the native <select> to icon-only | Cheapest, but visually cluttered by the browser's own dropdown caret, and a real accessibility cost for low-vision users relying on browser zoom |
| Two-row header, full labeled switcher below the primary nav | Safest for accessibility, but adds real height to every mobile page |
| Icon button that opens a popover panel (shipped) | Matches a verified precedent from a well-known documentation site's own dark-mode toggle |
The third option shipped, and building it surfaced real bugs no amount of code review would have found on paper — a Shift+Tab focus trap leaving a phantom open panel behind, and a keyboard path that bypassed a pending-state guard — both caught by testing the actual interaction live rather than reasoning about the ARIA pattern in the abstract.
Cutting a case study rather than describing proprietary work directly
The constraint itself — two substantive recent projects locked behind a former employer's proprietary, active-development repositories — is described in Context & goals, above. What's worth walking through here is what it produced. The centerpiece is an agentic rewrite of a legal-eligibility questionnaire, architected so the agent is structurally prevented from ever hallucinating a legal outcome, with every result tool-derived and every reasoning step persisted for audit. It started as a take-home technical exercise for a company I was interviewing with at the time. I later generalized its framing on both GitHub and the site itself, so it reads as a standalone showcase project rather than revealing which company's process it came from, and built a live, publicly clickable demo of it rather than leaving it as a repository people have to run themselves.
Cutting a personal, vulnerable About-page narrative in favor of a credibility-first one
The first full draft of the About page ran long and personal: a full origin story, an admission of self-doubt across nearly every job, a specific "here's a mistake I made and cleaned up" anecdote. My own reaction was that it was too long and too personal for a public page — I wanted people to be able to scan the highlights, not read a novel.
Dedicated research into how other AI-native consultants and freelancers actually position themselves publicly confirmed the instinct: lead with credibility and capability, not origin story; render career history as a scannable timeline, not narrative prose. And a genuine surprise in that research — the adversarial-review discipline that first draft treated as a minor process detail turned out to be exactly the kind of specific, checkable rigor that reads as a real differentiator in this space, while a generic self-doubt narrative was consistently absent from every credible example I found. The self-doubt material got cut entirely; the review-process material got promoted to a named principle instead.
Reflections
The most honest thing I can say about this process is that it isn't really about writing less code, or even writing code faster, though both are true. It's that the actual bottleneck moved. I used to get stuck in analysis paralysis before starting a personal project of real scope — the kind where the shape of the problem isn't handed to you, and you have to decide it yourself.
The loop I run now — deep planning, an adversarial pass on that plan from more than one angle, synthesizing the findings back into a revised plan, executing, then attacking the resulting code against that same plan before I ever look at it myself — is what actually unstuck that. Not because the agent is smarter than the analysis I would have done by hand, but because the structure gives me somewhere to put the uncertainty instead of carrying it as unresolved doubt before a single line exists.
That shows up everywhere in this build, not just in the big security or protocol decisions. The nested planning agent I dispatched for the mobile theme-switcher redesign correctly decided the discoverability-nudge component needed its own design pass before touching code, because there was no existing pattern in the codebase to copy — and I let that stand rather than overriding it, because that's exactly the judgment call the process is supposed to make on its own. The verifier that caught real defects on the very first PR is the reason I trust the eighteen PRs that came after it as much as I do — not because nothing ever went wrong, but because everything that did was caught before it reached me, and I can point to exactly where in the process it was caught.
I also don't think the process is free of real cost, and I'd rather say that directly than let the site's own confident framing paper over it. Verification and adversarial review consume real time and real model budget: this build ran through a genuinely large volume of background agent work, retried research when initial findings turned out to be wrong (the MCP protocol facts, most notably, corrected twice over the course of one PR), and accepted slower sequencing in exchange for a stack of small, independently reviewable PRs instead of one large diff I could ship faster but trust less.
I think that trade is the right one for anything that's going to represent me publicly or run in production, and I'd make it again. But it's a deliberate choice, not a side effect — the same kind of choice the rate-limiter tradeoff and the theme-pivot decision both were: name the cost, decide on purpose, and write down why, rather than letting either the tooling or the deadline make the call by default.
The site's real argument, in the end, isn't the multi-theme system or the MCP server or the security hardening individually. It's that all of it shipped through a process visible in the commit history itself: planned, reviewed from more than one angle before implementation, verified by something other than the thing that built it, and corrected in public when it was wrong. That's the actual demonstration. Everything else on the site is supporting evidence for it.