Skip to main content
  1. Articles/

Building a Satire Website Powered 100% by AI and Claude Skills

·2388 words·12 mins· loading · loading ·
Marko Milojevic
Author
Marko Milojevic
Software engineer and architect. Golang and LLM enthusiast. Awful chess player, gym rat, harmonica newbie and cat lover.

I was sitting on a balcony this weekend, and I was thinking how difficult today is to find a satirical newspaper that would fits the humor I like to hear. (I am sorry, I don’t want to sound arrogant, I just have very specific taste.) “If only there was such newspaper that would deliver a fresh article to me, every morning”. I mean, I can try to write something, but it requires a lot of time, and I would lose the most important piece of satire - surprise - the moment I write it. “If only… Bingo.”

Ask a large language model to write something funny, and it hands back a joke that would pass a corporate sensitivity review. The premise is safe, the punchline apologizes for itself, and the whole thing reads like satire written by someone worried about being fired. And then, we learn why that default voice is precisely the wrong tool for a project like Brain Epitaph: a satirical newspaper built to sound like a serious paper reporting on an insane world.

Brain Epitaph is a real, deployed site – fully designed, coded, and written by AI. It publishes a new front page (headlines, a lead story, obituaries, classifieds, corrections, letters to the editor) on a recurring schedule, daily. Building it meant solving two separate problems: a Next.js site that serves the content on Firebase, and a writing process that reliably produces satire someone actually finds funny. The second problem turned out to be the harder one, and Claude Skills is what solved it.

What a Claude Skill actually is
#

Before going further, we need the mechanism itself. A Claude Skill is a folder with a SKILL.md file at its root: a short YAML header (a name and a description) followed by a body of plain-Markdown instructions. Claude Code reads every skill’s header up front, matches the current request against each description, and only pulls the full body into context when a skill is actually relevant. Nothing about a skill is exotic — no special runtime, no plugin API. It is instructions on disk, versioned like any other file in the repository, that Claude loads by name instead of the user retyping them.

That last part is its entire value. A prompt lives once, in one conversation, then disappears. A skill lives in the repository, and gets refined the same way any other file does: notice what’s wrong with it, then fix it. Every future session starts from that refined version instead of the original one.

Building and tuning the skills themselves
#

Every skill described later in this post (the ones that actually write and publish Brain Epitaph’s content) started as output from two meta skills that sit one level up. They do not write satire or touch Firebase themselves; they build and refine the skills that do. I plan to cover these two skills more in detail in the future articles, so stay tuned.

skill-creator is what we reach for whenever a new skill needs to exist, or an existing one needs restructuring. It runs its own small process: describe roughly what the skill should do, draft it, write a handful of test prompts, run Claude against the draft with those prompts, and judge the results before committing to the file. Every one of the four Brain Epitaph skills below started this way rather than as a freehand Markdown file.

SKILL.md frontmatter — skill-creator

---
name: skill-creator
description: Create new skills, modify and improve existing
skills, and measure skill performance. Use when users want to
create a skill from scratch, edit, or optimize an existing skill,
run evals to test a skill, benchmark skill performance with
variance analysis, or optimize a skill's description for better
triggering accuracy.
---

skill-autoresearch picks up after a skill already exists and is used to refine it, by small, incremental self-improvement steps - something that community could already see in The Poject of Anthony Karpathy.

SKILL.md frontmatter — skill-autoresearch

---
name: skill-autoresearch
description: "Autonomously optimize any Claude Code skill by
running it repeatedly, scoring outputs against binary evals,
mutating the prompt, and keeping improvements. Based on
Karpathy's autoresearch methodology. Use when: optimize this
skill, improve this skill, run autoresearch on, make this skill
better, self-improve skill, benchmark skill, eval my skill, run
evals on. Outputs: an improved SKILL.md, a results log, and a
changelog of every mutation tried."
---

Both of these skills are large enough, and interesting enough on their own, that they deserve more than a paragraph each — the phased interview skill-creator runs before writing a single line, and the mutate-score-keep loop skill-autoresearch runs afterward, are both worth their own future post. What follows here is how the skills they helped build actually work.

The wrong approach: prompting for tone every time
#

The naive way to produce satire with an AI model is to describe the tone in the prompt and ask for a draft. We can look at what that produces.

A Wrong Approach

Prompt: "Write a funny fake news headline about a company
apologizing for a data breach, in the style of The Onion."

Output: "Local Company Issues Heartfelt Apology, Promises
To Do Better, Definitely Learns From This One."

The output above is not wrong in any single word. It is wrong in aggregate — it is the default LLM comedy voice: tasteful, restrained, and a little sentimental. It winks at the reader instead of committing to the premise. Run this same prompt five times across five different topics, and five different problems appear:

  • The tone drifts session to session, because nothing anchors it beyond whatever words happen to be in that day’s prompt.
  • Feedback given on Monday’s draft has no way to reach Thursday’s draft — every session starts from the same generic baseline.
  • The model reaches for safety by softening the premise, which is the exact failure mode the project needs to avoid.
  • Nothing distinguishes a joke that works from one that does not, so nothing accumulates.
  • Scaling to a six-section front page multiplies the inconsistency instead of averaging it out.

A single good headline is achievable this way. A newspaper is not.

The solution: encode the voice, then tune it against real reactions
#

First Issue
First Issue

The principle here is straightforward: separate the knowledge of how it should sound from the act of asking for it, and put that knowledge somewhere it survives between sessions. brain-epitaph-satire, the project’s voice skill, is exactly that — a bank of comic mechanics and worked examples, loaded fresh by name whenever anything satirical gets written for this project.

Its header is what makes Claude reach for it unprompted — the description spells out exactly which requests should trigger it, down to specific words like “funny” or “sarcastic”:

SKILL.md frontmatter — brain-epitaph-satire

---
name: brain-epitaph-satire
description: The tone, voice, and example bank for writing Brain
Epitaph satire — absurd, exaggerated, deadpan fake news in the
register of The Onion, the Babylon Bee, The Simpsons, Family Guy,
and South Park, written in plain B1–B2 English. Load this skill
whenever writing, rewriting, or judging any satirical text for
this project... Do not write satire for this project from
instinct — the default LLM comedy voice is tasteful, restrained
and sentimental, which is precisely wrong here.
---

The last sentence of that description is doing real work — it names the exact failure mode from the wrong approach above and tells Claude, at the moment of deciding whether to load this file, that instinct is not good enough here.

The skill’s own law states the core constraint plainly:

Reality is the starting line, not the material. Never file a piece that is standing on it.

Every mechanic beneath that law came from writing real drafts and getting a real reaction — not from guessing what “funny” means in the abstract. We can look at one of them directly.

Voice Skill Excerpt

**1. Insane premise, professional face.** The wilder the claim,
the more boring the sentence delivering it. Panic is the
reader's job, not the writer's.

> The Ministry confirmed the shortage is now retroactive and
> will affect last year as well.

**4. Fake methodology is the best joke in any fake study.**
Never hide how the research was done — the research *is* the
punchline.

> The study surveyed 200,000 men, all of whom measured
> themselves, alone, at home, and were asked to be honest.

Notice that neither example above hedges. The sentence delivering an impossible claim stays flat and procedural, and the fake research method is stated outright instead of glossed over. That is the difference a skill makes over a prompt: the rule was not re-derived for this article, it was already sitting in the file, tested against dozens of prior drafts.

The variation: skills as a workflow, not just a voice
#

Second Issue
Second Issue

A single voice skill solves consistency for one piece written by one pass. It does not solve a second problem: even with the right voice loaded, a first draft has blind spots — a joke that only works if the writer already knows the premise, a section that quietly repeats an earlier one, a line the writer is too close to see as flat. That problem needed a different kind of skill: one that describes a process, not a tone.

brain-epitaph-newsroom is that skill, and it stays deliberately separate from the voice skill. Its job is orchestration only — who writes, who reads, in what order.

SKILL.md frontmatter — brain-epitaph-newsroom

---
name: brain-epitaph-newsroom
description: Runs the six-agent pipeline that produces a Brain
Epitaph front-page issue — the first agent scouts today's topic
using brain-epitaph-topic-scout, the second writes a draft using
the brain-epitaph-satire skill, a third critiques it (rule
compliance and a cold reader's-eye pass, both in one report), a
fourth turns that critique into a revision, a fifth critiques the
revision fresh, and a sixth makes the final correction pass...
This is the standard way front-page issues get produced in this
project; do not draft a front page solo, and do not write satire
from instinct, when this applies.
---

That description already names the other two skills this pipeline depends on. Stage zero is one of them — before the writer or the output folder even matters, a dedicated research skill picks the day’s topic:

SKILL.md frontmatter — brain-epitaph-topic-scout

---
name: brain-epitaph-topic-scout
description: Searches live news against Brain Epitaph's fixed
roster of twelve satire topics (references/topics.md) to find
each topic's strongest real-world example right now, judges them
for coverage breadth, recency, and how concrete/citable the hook
is, cross-checks candidates against the real-world stories used
in the last 4-5 published issues... then picks the single best
topic and news hook for the next issue's main article... Research
and selection only: it never writes satire and never touches
Firestore.
---

We can see the full pipeline this arrangement produces:

Newsroom Pipeline

0. Topic scout   — scans live news against a fixed topic roster,
                    picks one with a real, citable hook.
1. Writer        — loads brain-epitaph-satire, drafts the full
                    front page from the scout's pick.
2. Critic round 1 — rule-compliance pass + cold reader's-eye pass,
                    against the draft only, never the writer's notes.
3. Revision      — acts on the critic's report, rule compliance
                    wins over reader preference on any conflict.
4. Critic round 2 — fresh read of the revision, no memory of round 1.
5. Correction    — final pass, folds in round 2's findings.
6. Present       — hands the finished issue back for review.

Each stage is a separate agent that starts cold, on purpose. The round 1 critic never sees the writer’s reasoning in notes.md, only the finished draft — reading the writer’s intentions would bias the critic toward agreeing with them instead of judging the page a reader would actually see. The round 2 critic never sees round 1’s report, for the same reason: a fresh read is the only way to confirm a fix actually landed, instead of confirming that the reviser did what it was told.

This is where the project’s skills earn the separation. brain-epitaph-satire never mentions who reads a draft or in what order. brain-epitaph-newsroom never redefines a single tone rule — it only cites the voice skill by name and lets each agent load it fresh. A tone problem gets fixed in one file; a process problem gets fixed in another. No skill drifts to cover a different skill’s job.

Publishing the result stays a fourth, separate skill again:

SKILL.md frontmatter — brain-epitaph-firestore-publish

---
name: brain-epitaph-firestore-publish
description: Publishes finished Brain Epitaph issues from
issues/{yyyy-mm-dd}/ into the Firestore "issues" collection that
the Next.js site (web/) reads its front page from. Use this
whenever Marko says to publish issues, push issues live, sync
issues to Firestore, update the site with the latest issue, or
get the newsroom output onto the site — the newsroom skill only
writes markdown files to disk, it never touches Firestore, so
this is always the separate step... Safe to run repeatedly: it
only adds issues that aren't in Firestore yet and never
overwrites or duplicates one that's already there.
---

This skill reads the highest-numbered draft in an issue’s folder, parses it, and writes it to Firestore, skipping any folder already published so the operation is safe to rerun. The site itself — a Next.js App Router app on Firebase App Hosting — reads the newest published issue on a sixty-second revalidation window, so a new issue goes live without a redeploy. None of that infrastructure needed AI generating the workflow at request time; it needed the workflow written down once, the same principle as the voice skill, just applied to operations instead of comedy.

Conclusion
#

The pattern across every skill in this project is the same one: stop asking the model to reconstruct expertise on every request, and instead write that expertise down once, in a file that gets loaded by name and refined against real outcomes. For tone, that turned inconsistent one-off prompts into a voice tuned specifically against what one person finds funny. For process, a single fallible draft became a six-stage pipeline where cold, fresh reads catch what a tired writer would miss, and even the instructions behind it get built and hardened the same disciplined way rather than freehand. The output is disposable; the skills that produce it are not.

Related

LLM and Go: Investigating Anthropic Messages API

·2276 words·11 mins· loading · loading
In the previous article I covered the fundamentals of Anthropic’s Messages API: setting up a client, maintaining conversation history, and integrating tools. That was enough to build a working conversational agent. This article goes a level deeper — into the API parameters that shape what the model returns and how it thinks. Two parameters stand out as particularly useful in production: output_config.format and output_config.effort. The first gives you control over the structure of the model’s output. The second controls how much the model reasons before responding — which turns out to matter more than you might expect once you start caring about latency and cost. Messages API details # The Messages API endpoint accepts a rich set of parameters. Most have sensible defaults and you will rarely touch them, but understanding what is available saves you from reaching for workarounds that already exist in the API. The table below covers a selection of the current parameters from the API reference: Parameter Type Description model string ID of the model to use messages array Conversation history as an ordered list of messages system string/array System prompt that sets the model’s behaviour, kept separate from messages max_tokens integer Maximum tokens the model may generate — required on every request output_config.format object Constrains the response to a JSON Schema output_config.effort string Reasoning depth: low, medium, high, xhigh, max thinking object Enables and configures extended or adaptive thinking temperature number Sampling temperature from 0 to 1; higher values produce more random output top_p number Alternative to temperature; nucleus sampling probability mass top_k integer Restricts sampling to the top K most likely tokens stop_sequences array Custom sequences at which the API stops generating stream boolean Stream partial responses as server-sent events tools array List of tools the model may call tool_choice object Controls which tool the model calls metadata object Arbitrary metadata about the request, such as an end-user ID In this article we focus on output_config.format and output_config.effort — two parameters with a direct, visible impact on production systems. Information extraction with output_config.format # The format field inside output_config controls how the model structures its output. By default, Claude replies with plain text. Setting output_config.format to a json_schema document constrains the response to conform to that schema — Anthropic calls this structured outputs.

LLM and Go: Anthropic Integration via Messages API

·2757 words·13 mins· loading · loading
The previous two articles in this series covered OpenAI’s side of this problem: Chat Completions, where the client owns the entire conversation history, and Responses, where OpenAI’s servers do. Anthropic’s Claude models are built on a third set of API decisions — close enough to Chat Completions in shape that the same agent design carries over almost directly, but different enough in the details, like how the system prompt is passed and how a response is structured, that it is worth building the same agent again to see exactly where. This article rebuilds that agent on Anthropic’s Messages API — the stateless, request-based interface behind Claude. By the end, you will have a working conversational agent that can call external tools to answer questions it otherwise could not, and a clear picture of what changes when you swap providers. A short introduction to Claude and Anthropic # The path to large language models runs through a decade of incremental progress in deep learning. Early models like word2vec and GloVe learned to embed words into dense vector spaces, capturing semantic relationships between terms. The transformer architecture, introduced by Google in 2017, changed the trajectory of the field — it processes sequences in parallel using attention mechanisms that capture long-range dependencies far more effectively than recurrent networks. This architectural shift made it practical to train models on orders of magnitude more data. GPT-1 in 2018 showed that large-scale unsupervised pre-training followed by fine-tuning could match or beat purpose-built models across a range of language tasks. Understanding what these models actually do removes a lot of the mysticism around them. An LLM is, at its core, a next-token predictor. It takes a sequence of tokens as input and outputs a probability distribution over the vocabulary for the next token. The transformer’s attention mechanism allows every token in the input to attend to every other token, building a rich contextual representation before making that prediction. Training adjusts billions of parameters to minimise prediction error across enormous text corpora. What emerges is a model with broad world knowledge encoded in its weights — not because it was taught facts directly, but because predicting text well requires internalising the structure of the world that produced that text. Claude is Anthropic’s model family, and the company itself was founded in 2021 by a group of former OpenAI researchers, including Dario Amodei and Daniela Amodei, with AI safety as its founding focus. What differentiates Claude’s training pipeline from a pure RLHF approach is Constitutional AI — a technique where the model critiques and revises its own outputs against a written set of principles, rather than relying solely on human raters to judge every response. The goal is a model that behaves predictably even in situations no human rater explicitly labelled. For developers, the way to reach any Claude model programmatically is the Messages API — the stateless, request-based interface used throughout this article.

LLM and Go: Retrieval-Augmented Generation (RAG) with OpenAI - Part 2

·2335 words·11 mins· loading · loading
In the previous article we built a feeder application: it reads fairy tales from a Markdown file, generates an OpenAI embedding for each one, and stores the text, the vector, and a small metadata blob in Postgres through pgvector. That gave us a table full of vectors, but no way to query it from a running application — the data sat there, searchable only by hand, through a database client. This article closes that gap. We build a small conversational agent that searches the fairy tale library through a tool and answers using only what that tool returns, not whatever GPT happens to remember about fairy tales from its training data. The model decides on its own when a search is needed, based on what the user actually asks. Setting Up The Project # Everything here builds on top of the previous article, so the same Docker Compose setup (a Postgres 17 container with the pgvector extension baked in) needs to be running, with the fairy_tales table already populated by the feeder. If that step has not happened yet, the first article walks through the Docker Compose file, the table schema, and the feeder application that fills it with fifty fairy tales. Assuming the feeder already ran, the table looks like this: Fairy tales table in pgvector The data is in place, but the goal for this article is different from a one-off SQL query: we want an AI agent to decide, on its own, when a search is needed and what to search for. Running a hardcoded query against every user message would not give the model that choice. That means giving the agent a tool — a function with a name, a description, and a parameter schema — that it can choose to call. The tool itself is allowed to call the OpenAI API. The agent calls GPT to decide it needs information, and the tool it invokes then calls the Embeddings API on its own to turn that request into a vector before it ever touches Postgres. Nothing forces every OpenAI call in an application through the same layer: a tool is just a Go function, and that function can talk to whatever it needs to get its job done.