From Zero to Live: A Decision Log of Building x-li.org with AI

· Blog AIDjangoTailscaleVPS

Where it started

I had an empty VPS and an unused domain. The goal was simple: a personal tech blog with some tools I actually need for research and everyday work, and a server that could double as a playground for AI agents.

Before touching anything, I set three hard requirements:

  1. Use only the monthly API credits that come with my Max subscription: no extra spending, and no eating into my regular chat usage.
  2. Keep my own interaction minimal: day to day, I want to talk to an agent in the browser and click "approve", not type SSH commands.
  3. Keep the structure clean: if I ever stop using an agent, I must be able to understand and maintain everything myself, and the site must keep running. The agent is a maintainer, not a runtime dependency.

And one security red line: it is acceptable for the VPS to be compromised, but it must never become a stepping stone into my own computer. Nothing important lives on the server; if it breaks, I reinstall. My PC is a different matter.

That red line ended up driving many of the later decisions.

The first big turn: who does the work?

The original idea was to keep Claude Code running on the VPS and drive it through Remote Control from the web or my phone. Two problems came up:

  • Wrong budget: the API credits included with the subscription cover the Claude API, the Agent SDK and Managed Agents, but not interactive Claude Code. Remote Control would draw on my subscription usage, not on those credits.
  • Credential risk: Remote Control needs a full account login stored on the VPS. If the VPS were compromised, that credential could be used against other things on the same account, which runs straight into the red line.

We compared alternatives: Claude Code in -p mode with an API key (uses the credits, but only as one-shot commands in a terminal), and the open-source Hermes Agent (has a web UI, but had just had vulnerabilities in its command-approval logic disclosed). The final choice was Claude Managed Agents with a self-hosted sandbox:

  • The model runs on Anthropic's side; the VPS runs only a small worker that makes outbound connections only, picks up tool calls, executes them on the server and posts the results back.
  • The only secret on the VPS is an environment key, which can only pick up work for that one environment and cannot touch my account.
  • Usage is billed against the API credits.
  • I can chat, watch every step and approve actions in the browser.

There was a detour along the way: for a while, "let the Claude on my PC control the VPS remotely" looked like the most convenient option. But then a local agent would be reading output that comes from the VPS, and a compromised VPS could hide instructions in that output. The rule we settled on: no agent on my PC both reads VPS output and can execute commands locally.

Networking: lock first, then open the door

Every change that could lock me out (firewall, SSH, Tailscale, network configuration) followed the same anti-lockout procedure: confirm that two independent management channels work; schedule an automatic rollback before applying the change; cancel the rollback only after I have verified from my own PC that I can still log in.

Key decisions:

  • The Tailscale access policy denies everything by default. Only my devices may reach the VPS's SSH and file-sharing ports, and the VPS cannot initiate connections to any other device. My PC also has Tailscale's incoming connections switched off, a lock that does not depend on any admin-console policy.
  • A surprise: my tailnet wasn't new. It still held several old servers, and the policy was still the default "allow all". I cleaned up the devices and rewrote the policy before the VPS was allowed to join.
  • Only ports 80 and 443 are open to the internet. SSH and file sharing go over the private network only.
  • Testing showed that Tailscale's own firewall chain is evaluated before ufw, so ufw does not actually restrict the private side. That side is restricted by the Tailscale policy instead, which lives in the admin console where the VPS cannot change it, exactly what the red line asks for.

The website: standard and hand-maintainable beats "optimal"

The requirements: a blog plus tools; I am the only one who needs to log in; visitor accounts are created by hand; content is written by the agent and published directly.

  • Django: one framework covers the blog, tools, login and database, and it ships with an admin interface, so without an agent I can still edit content in the browser. Everything is installed from official Ubuntu packages, so security updates arrive with the system.
  • A small debate about the WSGI server: cheroot, in Ubuntu's main repository, has an official security-update commitment; gunicorn, the most standard choice, is in universe and does not. The initial recommendation was cheroot, but I valued "standard, well-documented, and something I can maintain myself without an agent" more, so we went with gunicorn. There is no universally right answer to trade-offs like this. What matters is writing the reasons down.
  • AI-written content needs an extra security layer: articles may be based on web material, and web pages can hide malicious instructions. So raw HTML in Markdown is always escaped, and a strict Content Security Policy forbids inline scripts. The footer says that some content is AI-generated.

Research tools: review the spec before writing code

Beyond a general unit converter, I wanted tools I actually use: concentration conversion and solution preparation (mol, ppm, ppb, µg/mL), spectroscopic unit conversion, frequency and time, AFM scan parameters, single-molecule volume and mass, Beer–Lambert and protein A280, and gas ppm versus mg/m³.

Tools like these are only worth anything if they compute correctly, so the agent first delivered a specification (formulas, assumptions, validity conditions, sources for constants and test cases), and code was written only after review. The review did catch problems:

  • In AFM tip deconvolution, the validity criterion for the "base width" mode was wrong: it reused the formula from the "half-height width" mode. The corrected criterion is much stricter.
  • The spec treated mmHg as exactly defined; only the torr is exact by definition.
  • Protein presets were not written from memory: sequences were taken from UniProt, and ε280 and molecular weight were recomputed with the coefficients of Pace et al., matching ExPASy item by item. The widely quoted BSA value of 43 824 turned out to be a vendor figure converted from E1%, so it was not used.
  • "Protein density 1.35 g/cm³" is a common convention, but the literature ranges from about 1.22 to 1.41, and the page says so. "DNA density 1.7" is actually the buoyant density of DNA in CsCl, not the density of the molecule itself, so that preset was removed.
  • Many Europeans write a comma as the decimal separator. The tools accept 1,5, but reject ambiguous input such as 1,000 (which may mean one thousand), and always show the value actually used next to the input field.

Publishing without an agent

To meet the third requirement, we added an "inbox": I drop a Markdown file with a small metadata header into a shared folder, the server scans it every five minutes and creates or updates the article, and the result (success or failure, and why) is written back to the same folder. The inbox can add and update articles but never delete them, to avoid accidents. This article was published that way.

Pitfalls

This may be the most useful part:

  • The invisible BOM: when Windows PowerShell pipes text to ssh, it can prepend an invisible character. With that character in front of the key, the service kept returning 401.
  • Swallowed quotes: PowerShell 5.1 drops embedded double quotes when passing arguments to external programs. From then on, remote commands sent through ssh never used double quotes.
  • pkill killed itself: pkill -f some-name also kills the shell running it, because the shell's own command line contains that name. The [x] pattern trick avoids this.
  • A pager waiting for Enter: the agent ran git diff, which opened a pager and sat at "Press RETURN" until the command timed out.
  • A 9.3 GB memory peak: during mutation testing, one mutation raised a file-size limit to 10¹⁰ bytes, and a test duly built 10 GB of data. When memory ran out, systemd's default policy stopped the whole worker service, the running command was lost, and the session hung. The fix: a memory limit for the worker, an OOM policy that kills only the offending process, and long jobs always run in a separate transient service with its own memory cap.
  • Two extra minutes at boot: networking is managed by ifupdown, but systemd-networkd was also enabled without managing any interface, and its "wait for network" step timed out after two minutes on every boot. After masking it, the network is ready in 3 seconds instead of 2 minutes.

About cost

On the day of the build, the agent used roughly a third of the monthly credits (about 33 USD). The money went less into the work itself than into long sessions: once the context grew past 300k tokens, every call re-read it, and any pause of more than a few minutes expired the cache, so the whole context had to be written again. The lesson: one task per fresh session, with the full requirements stated up front.

Who did what

In the end, the division of labour looked like this:

  • Me: setting goals and red lines, making every decision, verifying results from my own PC.
  • Claude (planning and review): discussing options, reviewing the agent's plans, looking up literature and checking numbers, and writing the task briefs for the agent.
  • The agent on the VPS: executing on the server, testing, and writing the operations manual.

A few rules that kept the collaboration smooth: anything I need to decide comes as a multiple-choice question with a recommended option; no password or key ever appears in a conversation; research is not delegated to the agent, which saves its credits.

What's next

First, actually use the site and the tools. The agent playground will live in a fully isolated environment, planned once I have used things for a while and know what they really cost.

This article was compiled by AI (Claude) from the conversation records of the build, and published after review by the author.