Skip to content
← All essays

From a PDF generator to a portal in 5 languages: six months of vibecoding pkmntools

Today pkmntools.com is a Pokémon TCG portal in five languages, with an editorial blog, live card prices, a collection tracker backed by its own database, and continuous deployment to my own VPS. Six months ago it was a PDF generator built in a weekend.

pkmntools.com today, on desktop and mobile

Before telling you how that happened, a disclaimer that matters to me: I am not a professional developer. I’m a solutions architect. My job is designing systems, not writing code eight hours a day. Everything that follows was built by vibecoding: defining what I wanted, why, and under which constraints, and letting AI agents write most of the code under my direction. Of the project’s 535 commits, 370 are co-authored by Claude. I’m not hiding that; it’s the whole point of this article.

The short version, in case you decide to stay: from 10 Google impressions to over 55,000 in six months, users multiplied by 15, an API block that took down half the site, zero money earned, and a hosting bill that ended at zero. This is the full story, mistakes included.

January: a $16 million auction and €3 placeholders

In January this year I was feeling a bit lost. Big changes were coming in my life, and on the nights I couldn’t fall asleep early I kept circling two parallel searches: a personal project that would help me learn, practice and explore areas that could boost my career, and a hobby that would let me share more time with my daughter, Fernanda: something fun, something that would take us time, something that would leave lasting memories of our relationship.

The first wasn’t easy. I’d start projects without real direction or inspiration, and without a concrete use it was hard to find them a north. The second found me: one day, scrolling Instagram, I ran into the hype around Logan Paul’s Pikachu Illustrator auction, which closed at 16.5 million dollars, the all-time record for a collectible card. (Months later, that very card would end up headlining pkmntools’ most-read article; life takes funny turns.) That took me back to Pokémon: the anime I watched as a kid and teenager, and the cards I’d seen around but that never caught my attention.

After weeks of research and a lot of talking it over with Gemini and Claude, I made the call: I bought my first Ultra Premium Collection and a Battle Academy to enter the Pokémon world with Fernanda. A couple of months later, between sorting cards, searching the internet and deciding how to take the collection forward, I hit the need for binder placeholders… and paid €3 for them. I was left with the frustration of having paid for something so “dumb” and simple I could have generated myself. That’s where the idea of having my own Placeholder Dex was born.

April: the PDF generator (or the myth of “build it and they will come”)

With the idea clear (a placeholder card PDF generator for collection binders), I scaffolded it with create-next-app, Tailwind, and a deploy to Vercel: the path of least resistance for someone who wants results, not friction. Thirty-eight commits shipped in four days: a TCG color palette, GTM analytics, even a Ko-fi donation button, because in my head the site was going to take off on its own.

April closed with 120 users and 10 impressions on Google. Ten.

May: the month it almost died

There isn’t a single commit in May. The metrics say it all: 22 users, 8 impressions, zero organic clicks.

The truth is it wasn’t just lack of time, though time was scarce too: I have a demanding job, a family to provide for and be present with, and right then I had to relocate to another city for work. But the deeper reason was something else: I didn’t know how to grow the site organically. I believed putting it online was enough, and it wasn’t. Nobody arrives at a site nobody links to and Google has no reason to show.

Out of that frustration came the idea that changed everything: if the problem was that there were no reasons to arrive at the site, I had to create them. Content. A blog.

June: the editorial pivot (and the technologies, with their why)

On June 3rd the project came back to life, and that month’s decisions defined everything that followed. I’ll walk through them with their reasoning, because that’s where the real learning is:

A blog in MDX, no CMS. Articles live as .mdx files in the repository, with embedded React components (card galleries, price tables, timelines). Why no CMS? Because the editor is me plus an AI agent in editorial sessions: we research a topic, write, and the article lands as a commit like any other code. A CMS would have added infrastructure for a workflow that doesn’t exist.

Server-side i18n, not client-side. In April I had built a naive i18n with localStorage: the user switched languages and the text changed in the browser. To Google that’s invisible: only one page exists. In June I rebuilt it properly: per-language routes (/en/, /pt/, /fr/, /de/), server rendering, and reciprocal hreflang between translations. Five languages, five indexable sites.

Live prices with ISR. Articles show current card prices via the public TCGdex API, regenerated with Next.js Incremental Static Regeneration. The project’s most important editorial rule: the price an article was published with is never edited, because it’s historical evidence. Live prices are shown separately, labeled as such.

Custom MCPs for the agents. This is where my architect role weighed more than any line of code. If AI agents were going to be my development team and my editorial team, they needed structured access to domain data. I built MCP (Model Context Protocol) servers exposing card identity, sets and prices as tools any agent can call. Writing an article about a set went from “copy-pasting data” to “the agent queries the cards and their prices itself”.

The pkmntools blog today: editorial, bilingual, with live prices

That same month the project outgrew its repository: a card store built on MedusaJS with its own storefront was born, plus an AI-vision cataloger for physical cards, and the infrastructure stack on a VPS. The store was, above all, an experiment to try the platform (I wanted first-hand experience of what building commerce on Medusa feels like), but it may eventually become a new growth avenue for the project. That story deserves its own article; back to the portal.

July: the J-curve

On July 26th I turned on the pilot Portuguese and German translations with full reciprocal hreflang. Within four days, daily impressions jumped from 162 to 424. Germany became my #1 market: the German homepage became the site’s most visited landing page, riding “platzhalter pdf” searches with no competition.

July closed with 274 users and 4,607 impressions. Organic, which was 5% of traffic in April, was on its way to 72%. I’ve repeated the lesson to myself ever since: well-structured multilingual content was the most profitable growth lever of the entire project, and it cost a fraction of any feature.

August: 248 commits and the month that taught me humility

August was the most intense month: 248 commits. The Collection Tracker shipped: track your collection, with Google login or an OTP code by email (Auth.js + Resend), shareable public lists, and a monthly email digest. Underneath, a Postgres database with Drizzle as the ORM and the first cron jobs.

The Collection Tracker: mark your cards and watch your collection's live value

It was also the month of the mistakes that taught me the most:

The bug the tests couldn’t see. The pricing API uses the key reverse-holofoil, with a hyphen. My code read it in camelCase. Result: reverse holo prices were silently discarded across the entire site. Tests passed because the mocks used the same wrong key. Moral: a test that mocks your assumption only verifies you’re still assuming the same thing.

The AI left its signature in production. A subagent that translated an article into French left its internal tool-call formatting tags at the end of the file. The MDX didn’t compile and the article returned a 500 in production. Vibecoding doesn’t exempt you from reviewing what ships; it obliges you to review it better.

Respect third-party APIs. A mistake of mine multiplied calls to an external API until I got temporarily blocked, and the parts of the site depending on that data went down for two days. The fix was architectural, not apologetic: a global request throttle and a circuit breaker. The error rate went from 20.3% to 0.6% and those pages’ p95 dropped from 10 seconds to 89 milliseconds.

September: migrating to my VPS

With traffic growing, the Vercel bill grew too. A single oversight illustrated it: one headers() call in the root layout made rendering dynamic for the entire site: every visit to a heavy page was served from the origin, and the invoice showed it.

I already had a VPS (Hostinger, 2 vCPU / 8 GB) running other services with Docker and Traefik as the TLS proxy. The decision was an architect’s call: why pay for premium serverless hosting for a mostly static site, when I had idle capacity already paid for?

The migration happened in two steps, each reversible:

  1. Crons first, from Vercel to GitHub Actions. That alone allowed downgrading the plan.
  2. Then the whole site: a multi-stage Dockerfile with Next.js standalone output, builds on GitHub Actions, the image pushed to GitHub Container Registry, deploys to the VPS with a health gate (if /api/health doesn’t answer, there’s no cutover), and Cloudflare as the CDN in front. The rollback plan was flipping a DNS record: under 5 minutes.

A set page served from the VPS: HeartGold & SoulSilver

It went cleanly. No downtime, no damage to Google’s crawling, and better latency than before (p95 ~65 ms). Hosting cost went from about USD $22/month to practically $0 marginal, because the VPS already existed. Since September 20th, every push to main builds and deploys itself.

The honest numbers

I promised transparency, so here are both sides.

What worked:

What didn’t work (yet):

What I take with me

Six months later, this is what I’d tell April me:

  1. Shipping is not growing. Without a reason to be found (content, in my case), the best product is invisible. The blog wasn’t an extra: it was the business.
  2. AI multiplies whoever directs; it doesn’t replace whoever thinks. My best contributions to the project weren’t code: they were clear specs, well-placed constraints (“the published price is sacred”), and giving agents the tools (MCPs) to work with real data.
  3. The expensive mistakes are architectural, and so are the cheap ones. A misplaced headers(), a hyphenated key, a missing circuit breaker. None of that is fixed by “more AI”; it’s fixed by understanding the system.
  4. Own your infrastructure when the numbers ask for it. Serverless was perfect to start and expensive to grow. The VPS with Docker, Traefik and a CI/CD pipeline cost me a weekend and gives me back $22 every month.

The project is still alive, with more open questions than closed ones: retention, monetization, a scanner app on the horizon. If any of this helped you, or if you’re another architect vibecoding on the side, write to me. The next story will probably be the store’s.