Repository files navigation

Halupedia

"Comprehensive coverage of topics mainstream encyclopedias overlooked."

An infinite, hallucinated encyclopedia. Every link leads to an entry that does not exist yet — until you click it, at which point an LLM pretends it has always existed and writes it for you, in the deadpan register of a 19th-century scholarly press.

Live at halupedia.com. Cooked on a Cloudflare Worker. Cached forever in KV. Threaded HN-style comments under every article, no signup, AI-hallucinated identities. Patrons may buy us tokens so the press can keep printing. Editors and conspirators meet in the Discord.


Table of contents


What it is

Halupedia is a single-page Cloudflare Worker that:

  1. Serves a React SPA that looks like an old print encyclopedia.
  2. On a request for any unknown slug, calls an LLM (via OpenRouter) that returns an HTML article in the encyclopedia's voice — full of confident, plausible-sounding nonsense that is densely cross-linked to other entries that also do not yet exist.
  3. Caches that article in Cloudflare KV forever. Subsequent visits are free.
  4. Lets readers leave HN-style threaded comments without ever signing up. Names are hallucinated by the LLM on first comment and tied to a cookie.

There is no editorial staff, no truth, no warranty. Every article is invented on demand. The footnotes are also lies.

Star history

Star History Chart

How a page is born

 you click ─► /api/page/footnote-drift
│
▼
┌──────────────────┐
│ in KV already? │── yes ─► stream from KV (free)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ are you a bot? │── yes ─► 404 (no token spend)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ over IP rate? │── yes ─► 429 with Retry-After
└──────────────────┘
│ no
▼
┌──────────────────────────────────┐
│ load prior link-hints from D1 │
│ (canon set by other articles) │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ stream LLM via OpenRouter │
│ → split stream: │
│ a) sanitize + send to client │
│ b) collect, persist to KV, │
│ extract & save link hints │
└──────────────────────────────────┘

The HTML stream is split (ReadableStream.tee()) so the user starts reading the article while the worker is still receiving and persisting it. First paint is sub-second; the worker continues writing to KV under ctx.waitUntil() after the response closes.

Consistency in a hallucinated universe

The hardest problem with an infinite, on-demand encyclopedia is internal contradiction: article A says Mortimer Vellum died in 1843; article B, generated three weeks later, says he was alive in 1881. Halupedia solves this with link hints:

  • When the LLM writes an article, it is required to add a context="…" attribute on every <a> it inserts, summarising the future article it is linking to (e.g. context="19th-century clerk who formalized footnote drift, Pellbrick's mentor").
  • Before serving the HTML, the worker harvests these context values into a link_hints table in D1, keyed by (target_slug, source_slug).
  • The context attribute is stripped before the HTML is sent to the browser — readers never see the metadata.
  • When that target article is later requested for the first time, the worker loads the accumulated hints and injects them into the system prompt as "PRIOR REFERENCES — these are CANON". The LLM is instructed that the encyclopedia is hallucinated and absurd, but it must not contradict itself.

The result is a write-forward consistency mechanism: each article seeds breadcrumbs for the entries it links to, so by the time those entries are written, the LLM has a small dossier of established lore to honour.

Comments

Hacker-News-style threaded comments under every article. Backed by Cloudflare D1 (free tier). Notable behaviours:

  • No signup, ever. The first time you post, the LLM hallucinates a name and username for you (e.g. Bartram Pellbrick-Thwaite / pellbrick_archivist), in the same scholarly register as the rest of the site. You are inserted into D1 with a UUID and given a hu_uid cookie.
  • Cookie is effectively permanent (capped at 400 days per RFC 6265bis, refreshed on every authenticated request — so active users never expire).
  • One upvote per comment per user, toggleable. Optimistic UI.
  • Threaded to arbitrary depth, sorted by score DESC, created_at ASC.
  • Author auto-upvotes their own post, so every comment opens at score 1.
  • Per-IP rate limit on identity creation so a botnet can't burn your budget by minting fresh hallucinated names in a loop.

Defenses against runaway costs

LLM tokens cost real money and Halupedia is run by one person who lacks a corporate Amex. The worker has a layered defense:

LayerCatchesImplementation
1. User-Agent regexHonest crawlers (Googlebot, GPTBot, ClaudeBot, curl, wget, scrapy, …)src/worker/index.tsisBot()
2. Per-IP article gen budgetUA-forging scrapers, runaway tabsKV-backed fixed-window limiter, GEN_PER_IP_PER_HOUR
3. Per-IP identity-mint budgetCookie-rotating spammers minting hallucinated namesIDENT_PER_IP_PER_HOUR
4. Global daily capDistributed botnets that defeat 1–3MAX_ARTICLES_PER_DAY, KV counter
5. Cache foreverSame slug never costs twiceKV put() with metadata
6. Tee-and-persistStream interruptions don't waste a generationReadableStream.tee() + waitUntil()
7. Cloudflare dashboardVolumetric / L7 attacksWAF rate-limit + Bot Fight Mode

Crucially: cached articles are served to everyone, including bots. The bot guard only fires on uncached slugs, so anything you've already paid to generate stays freely indexable for SEO.

Architecture

src/
├── worker/
│ ├── index.ts ← Hono app, request routing, generation pipeline
│ ├── llm.ts ← OpenRouter streaming client + system prompt
│ ├── sanitize.ts ← HTML allowlist + extracts link-hint metadata
│ ├── hints.ts ← D1 read/write for cross-article canon
│ ├── identity.ts ← LLM call that hallucinates {name, username}
│ ├── comments.ts ← Hono sub-app: threaded comments + voting + cookies
│ ├── ratelimit.ts ← Per-IP fixed-window KV limiter
│ ├── slug.ts ← Slug normalisation + reserved-slug list
│ ├── seed.ts ← Curated seed entries for the homepage
│ └── env.d.ts ← Worker env type
├── client/
│ ├── App.tsx ← SPA shell, history routing, streaming reader
│ ├── Comments.tsx ← Threaded HN-style comment UI
│ ├── AllEntries.tsx ← A–Z register of every article ever cached
│ └── styles.css ← Single hand-rolled stylesheet (parchment aesthetic)
├── shared/
│ └── … ← Types shared between worker & client
└── ...
migrations/
├── 0001_init.sql ← users, comments, votes
└── 0002_link_hints.sql ← (target_slug, source_slug) → blurb

Stack:

  • Cloudflare Workers — execution, runs everywhere, free tier covers viral.
  • Cloudflare KV — article HTML cache, stores {title, generatedAt} in metadata.
  • Cloudflare D1 — comments, users, votes, link hints.
  • Hono — small router + cookie helpers.
  • OpenRouter — LLM access (model is configurable via env var).
  • Vite + React 18 — SPA, no router library; history API by hand.
  • No build step on the worker. Wrangler bundles src/worker/index.ts and serves the Vite output as static assets via the ASSETS binding.

Local development

You will need: Node 20+, pnpm 9, and a Cloudflare account.

pnpm install
# Create a D1 database (one-time)
pnpm wrangler d1 create hallupedia
# Copy the printed database_id into wrangler.toml, replacing the placeholder.# Apply migrations locally
pnpm wrangler d1 migrations apply hallupedia --local
# Run vite (client) + wrangler (worker) concurrently
pnpm dev

Open http://localhost:8787. Articles will be generated on demand if you set your OPENROUTER_API_KEY (see below); otherwise the homepage seed will display but new entries will fail.

You can hit http://localhost:8787/api/index?refresh=1 at any time to force the total-entries counter to recount the KV namespace.

Deploying your own instance

# 1. Configure secrets
pnpm wrangler secret put OPENROUTER_API_KEY
# 2. Apply migrations to the remote D1
pnpm wrangler d1 migrations apply hallupedia --remote
# 3. Deploy
pnpm run deploy

The Worker handles its own routing, including /robots.txt, the SPA shell, and the API. If you bind a custom domain, edit the [[routes]] block in wrangler.toml. To deploy to a *.workers.dev URL instead, set workers_dev = true and remove the routes.

Configuration

Defined in wrangler.toml under [vars]:

VarDefaultPurpose
OPENROUTER_MODEL(set in toml)Model slug used for both article gen and identity hallucination
MAX_ARTICLES_PER_DAY5000Global circuit breaker — soft cap per UTC day
GEN_PER_IP_PER_HOUR100Per-IP article generation budget
IDENT_PER_IP_PER_HOUR10Per-IP cap on minting new commenter identities

Secrets (set via wrangler secret put):

SecretPurpose
OPENROUTER_API_KEYAuth for OpenRouter

Bindings:

  • ARTICLES — KV namespace for article HTML.
  • DB — D1 database for comments + link hints.
  • ASSETS — static assets (Vite build output).

Contributing

Pull requests welcome, especially anything that:

  • Reduces token spend per article without making the prose worse.
  • Improves cross-article consistency further.
  • Hardens the bot/UA defenses without breaking real readers.
  • Catches a "griffing" / prompt-injection vector you found in the wild.

Please open an issue first for anything user-facing so we can discuss tone — Halupedia lives or dies by its voice and an out-of-register entry is worse than no entry at all.

License

GPL-3.0. The source code in this repository is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

If the press has improved your day, you may buy us tokens or join the conversation on Discord.

About

Encyclopedia that hallucinates articles on the fly

Resources

Stars

244 stars

Watchers

4 watching

Forks

Sponsor this project

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Halupedia

"Comprehensive coverage of topics mainstream encyclopedias overlooked."

An infinite, hallucinated encyclopedia. Every link leads to an entry that does not exist yet — until you click it, at which point an LLM pretends it has always existed and writes it for you, in the deadpan register of a 19th-century scholarly press.

Live at halupedia.com. Cooked on a Cloudflare Worker. Cached forever in KV. Threaded HN-style comments under every article, no signup, AI-hallucinated identities. Patrons may buy us tokens so the press can keep printing. Editors and conspirators meet in the Discord.


Table of contents


What it is

Halupedia is a single-page Cloudflare Worker that:

  1. Serves a React SPA that looks like an old print encyclopedia.
  2. On a request for any unknown slug, calls an LLM (via OpenRouter) that returns an HTML article in the encyclopedia's voice — full of confident, plausible-sounding nonsense that is densely cross-linked to other entries that also do not yet exist.
  3. Caches that article in Cloudflare KV forever. Subsequent visits are free.
  4. Lets readers leave HN-style threaded comments without ever signing up. Names are hallucinated by the LLM on first comment and tied to a cookie.

There is no editorial staff, no truth, no warranty. Every article is invented on demand. The footnotes are also lies.

Star history

Star History Chart

How a page is born

 you click ─► /api/page/footnote-drift
│
▼
┌──────────────────┐
│ in KV already? │── yes ─► stream from KV (free)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ are you a bot? │── yes ─► 404 (no token spend)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ over IP rate? │── yes ─► 429 with Retry-After
└──────────────────┘
│ no
▼
┌──────────────────────────────────┐
│ load prior link-hints from D1 │
│ (canon set by other articles) │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ stream LLM via OpenRouter │
│ → split stream: │
│ a) sanitize + send to client │
│ b) collect, persist to KV, │
│ extract & save link hints │
└──────────────────────────────────┘

The HTML stream is split (ReadableStream.tee()) so the user starts reading the article while the worker is still receiving and persisting it. First paint is sub-second; the worker continues writing to KV under ctx.waitUntil() after the response closes.

Consistency in a hallucinated universe

The hardest problem with an infinite, on-demand encyclopedia is internal contradiction: article A says Mortimer Vellum died in 1843; article B, generated three weeks later, says he was alive in 1881. Halupedia solves this with link hints:

  • When the LLM writes an article, it is required to add a context="…" attribute on every <a> it inserts, summarising the future article it is linking to (e.g. context="19th-century clerk who formalized footnote drift, Pellbrick's mentor").
  • Before serving the HTML, the worker harvests these context values into a link_hints table in D1, keyed by (target_slug, source_slug).
  • The context attribute is stripped before the HTML is sent to the browser — readers never see the metadata.
  • When that target article is later requested for the first time, the worker loads the accumulated hints and injects them into the system prompt as "PRIOR REFERENCES — these are CANON". The LLM is instructed that the encyclopedia is hallucinated and absurd, but it must not contradict itself.

The result is a write-forward consistency mechanism: each article seeds breadcrumbs for the entries it links to, so by the time those entries are written, the LLM has a small dossier of established lore to honour.

Comments

Hacker-News-style threaded comments under every article. Backed by Cloudflare D1 (free tier). Notable behaviours:

  • No signup, ever. The first time you post, the LLM hallucinates a name and username for you (e.g. Bartram Pellbrick-Thwaite / pellbrick_archivist), in the same scholarly register as the rest of the site. You are inserted into D1 with a UUID and given a hu_uid cookie.
  • Cookie is effectively permanent (capped at 400 days per RFC 6265bis, refreshed on every authenticated request — so active users never expire).
  • One upvote per comment per user, toggleable. Optimistic UI.
  • Threaded to arbitrary depth, sorted by score DESC, created_at ASC.
  • Author auto-upvotes their own post, so every comment opens at score 1.
  • Per-IP rate limit on identity creation so a botnet can't burn your budget by minting fresh hallucinated names in a loop.

Defenses against runaway costs

LLM tokens cost real money and Halupedia is run by one person who lacks a corporate Amex. The worker has a layered defense:

LayerCatchesImplementation
1. User-Agent regexHonest crawlers (Googlebot, GPTBot, ClaudeBot, curl, wget, scrapy, …)src/worker/index.tsisBot()
2. Per-IP article gen budgetUA-forging scrapers, runaway tabsKV-backed fixed-window limiter, GEN_PER_IP_PER_HOUR
3. Per-IP identity-mint budgetCookie-rotating spammers minting hallucinated namesIDENT_PER_IP_PER_HOUR
4. Global daily capDistributed botnets that defeat 1–3MAX_ARTICLES_PER_DAY, KV counter
5. Cache foreverSame slug never costs twiceKV put() with metadata
6. Tee-and-persistStream interruptions don't waste a generationReadableStream.tee() + waitUntil()
7. Cloudflare dashboardVolumetric / L7 attacksWAF rate-limit + Bot Fight Mode

Crucially: cached articles are served to everyone, including bots. The bot guard only fires on uncached slugs, so anything you've already paid to generate stays freely indexable for SEO.

Architecture

src/
├── worker/
│ ├── index.ts ← Hono app, request routing, generation pipeline
│ ├── llm.ts ← OpenRouter streaming client + system prompt
│ ├── sanitize.ts ← HTML allowlist + extracts link-hint metadata
│ ├── hints.ts ← D1 read/write for cross-article canon
│ ├── identity.ts ← LLM call that hallucinates {name, username}
│ ├── comments.ts ← Hono sub-app: threaded comments + voting + cookies
│ ├── ratelimit.ts ← Per-IP fixed-window KV limiter
│ ├── slug.ts ← Slug normalisation + reserved-slug list
│ ├── seed.ts ← Curated seed entries for the homepage
│ └── env.d.ts ← Worker env type
├── client/
│ ├── App.tsx ← SPA shell, history routing, streaming reader
│ ├── Comments.tsx ← Threaded HN-style comment UI
│ ├── AllEntries.tsx ← A–Z register of every article ever cached
│ └── styles.css ← Single hand-rolled stylesheet (parchment aesthetic)
├── shared/
│ └── … ← Types shared between worker & client
└── ...
migrations/
├── 0001_init.sql ← users, comments, votes
└── 0002_link_hints.sql ← (target_slug, source_slug) → blurb

Stack:

  • Cloudflare Workers — execution, runs everywhere, free tier covers viral.
  • Cloudflare KV — article HTML cache, stores {title, generatedAt} in metadata.
  • Cloudflare D1 — comments, users, votes, link hints.
  • Hono — small router + cookie helpers.
  • OpenRouter — LLM access (model is configurable via env var).
  • Vite + React 18 — SPA, no router library; history API by hand.
  • No build step on the worker. Wrangler bundles src/worker/index.ts and serves the Vite output as static assets via the ASSETS binding.

Local development

You will need: Node 20+, pnpm 9, and a Cloudflare account.

pnpm install
# Create a D1 database (one-time)
pnpm wrangler d1 create hallupedia
# Copy the printed database_id into wrangler.toml, replacing the placeholder.# Apply migrations locally
pnpm wrangler d1 migrations apply hallupedia --local
# Run vite (client) + wrangler (worker) concurrently
pnpm dev

Open http://localhost:8787. Articles will be generated on demand if you set your OPENROUTER_API_KEY (see below); otherwise the homepage seed will display but new entries will fail.

You can hit http://localhost:8787/api/index?refresh=1 at any time to force the total-entries counter to recount the KV namespace.

Deploying your own instance

# 1. Configure secrets
pnpm wrangler secret put OPENROUTER_API_KEY
# 2. Apply migrations to the remote D1
pnpm wrangler d1 migrations apply hallupedia --remote
# 3. Deploy
pnpm run deploy

The Worker handles its own routing, including /robots.txt, the SPA shell, and the API. If you bind a custom domain, edit the [[routes]] block in wrangler.toml. To deploy to a *.workers.dev URL instead, set workers_dev = true and remove the routes.

Configuration

Defined in wrangler.toml under [vars]:

VarDefaultPurpose
OPENROUTER_MODEL(set in toml)Model slug used for both article gen and identity hallucination
MAX_ARTICLES_PER_DAY5000Global circuit breaker — soft cap per UTC day
GEN_PER_IP_PER_HOUR100Per-IP article generation budget
IDENT_PER_IP_PER_HOUR10Per-IP cap on minting new commenter identities

Secrets (set via wrangler secret put):

SecretPurpose
OPENROUTER_API_KEYAuth for OpenRouter

Bindings:

  • ARTICLES — KV namespace for article HTML.
  • DB — D1 database for comments + link hints.
  • ASSETS — static assets (Vite build output).

Contributing

Pull requests welcome, especially anything that:

  • Reduces token spend per article without making the prose worse.
  • Improves cross-article consistency further.
  • Hardens the bot/UA defenses without breaking real readers.
  • Catches a "griffing" / prompt-injection vector you found in the wild.

Please open an issue first for anything user-facing so we can discuss tone — Halupedia lives or dies by its voice and an out-of-register entry is worse than no entry at all.

License

GPL-3.0. The source code in this repository is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

If the press has improved your day, you may buy us tokens or join the conversation on Discord.

About

Encyclopedia that hallucinates articles on the fly

Resources

Stars

244 stars

Watchers

4 watching

Forks

Sponsor this project

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Halupedia

"Comprehensive coverage of topics mainstream encyclopedias overlooked."

An infinite, hallucinated encyclopedia. Every link leads to an entry that does not exist yet — until you click it, at which point an LLM pretends it has always existed and writes it for you, in the deadpan register of a 19th-century scholarly press.

Live at halupedia.com. Cooked on a Cloudflare Worker. Cached forever in KV. Threaded HN-style comments under every article, no signup, AI-hallucinated identities. Patrons may buy us tokens so the press can keep printing. Editors and conspirators meet in the Discord.


Table of contents


What it is

Halupedia is a single-page Cloudflare Worker that:

  1. Serves a React SPA that looks like an old print encyclopedia.
  2. On a request for any unknown slug, calls an LLM (via OpenRouter) that returns an HTML article in the encyclopedia's voice — full of confident, plausible-sounding nonsense that is densely cross-linked to other entries that also do not yet exist.
  3. Caches that article in Cloudflare KV forever. Subsequent visits are free.
  4. Lets readers leave HN-style threaded comments without ever signing up. Names are hallucinated by the LLM on first comment and tied to a cookie.

There is no editorial staff, no truth, no warranty. Every article is invented on demand. The footnotes are also lies.

Star history

Star History Chart

How a page is born

 you click ─► /api/page/footnote-drift
│
▼
┌──────────────────┐
│ in KV already? │── yes ─► stream from KV (free)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ are you a bot? │── yes ─► 404 (no token spend)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ over IP rate? │── yes ─► 429 with Retry-After
└──────────────────┘
│ no
▼
┌──────────────────────────────────┐
│ load prior link-hints from D1 │
│ (canon set by other articles) │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ stream LLM via OpenRouter │
│ → split stream: │
│ a) sanitize + send to client │
│ b) collect, persist to KV, │
│ extract & save link hints │
└──────────────────────────────────┘

The HTML stream is split (ReadableStream.tee()) so the user starts reading the article while the worker is still receiving and persisting it. First paint is sub-second; the worker continues writing to KV under ctx.waitUntil() after the response closes.

Consistency in a hallucinated universe

The hardest problem with an infinite, on-demand encyclopedia is internal contradiction: article A says Mortimer Vellum died in 1843; article B, generated three weeks later, says he was alive in 1881. Halupedia solves this with link hints:

  • When the LLM writes an article, it is required to add a context="…" attribute on every <a> it inserts, summarising the future article it is linking to (e.g. context="19th-century clerk who formalized footnote drift, Pellbrick's mentor").
  • Before serving the HTML, the worker harvests these context values into a link_hints table in D1, keyed by (target_slug, source_slug).
  • The context attribute is stripped before the HTML is sent to the browser — readers never see the metadata.
  • When that target article is later requested for the first time, the worker loads the accumulated hints and injects them into the system prompt as "PRIOR REFERENCES — these are CANON". The LLM is instructed that the encyclopedia is hallucinated and absurd, but it must not contradict itself.

The result is a write-forward consistency mechanism: each article seeds breadcrumbs for the entries it links to, so by the time those entries are written, the LLM has a small dossier of established lore to honour.

Comments

Hacker-News-style threaded comments under every article. Backed by Cloudflare D1 (free tier). Notable behaviours:

  • No signup, ever. The first time you post, the LLM hallucinates a name and username for you (e.g. Bartram Pellbrick-Thwaite / pellbrick_archivist), in the same scholarly register as the rest of the site. You are inserted into D1 with a UUID and given a hu_uid cookie.
  • Cookie is effectively permanent (capped at 400 days per RFC 6265bis, refreshed on every authenticated request — so active users never expire).
  • One upvote per comment per user, toggleable. Optimistic UI.
  • Threaded to arbitrary depth, sorted by score DESC, created_at ASC.
  • Author auto-upvotes their own post, so every comment opens at score 1.
  • Per-IP rate limit on identity creation so a botnet can't burn your budget by minting fresh hallucinated names in a loop.

Defenses against runaway costs

LLM tokens cost real money and Halupedia is run by one person who lacks a corporate Amex. The worker has a layered defense:

LayerCatchesImplementation
1. User-Agent regexHonest crawlers (Googlebot, GPTBot, ClaudeBot, curl, wget, scrapy, …)src/worker/index.tsisBot()
2. Per-IP article gen budgetUA-forging scrapers, runaway tabsKV-backed fixed-window limiter, GEN_PER_IP_PER_HOUR
3. Per-IP identity-mint budgetCookie-rotating spammers minting hallucinated namesIDENT_PER_IP_PER_HOUR
4. Global daily capDistributed botnets that defeat 1–3MAX_ARTICLES_PER_DAY, KV counter
5. Cache foreverSame slug never costs twiceKV put() with metadata
6. Tee-and-persistStream interruptions don't waste a generationReadableStream.tee() + waitUntil()
7. Cloudflare dashboardVolumetric / L7 attacksWAF rate-limit + Bot Fight Mode

Crucially: cached articles are served to everyone, including bots. The bot guard only fires on uncached slugs, so anything you've already paid to generate stays freely indexable for SEO.

Architecture

src/
├── worker/
│ ├── index.ts ← Hono app, request routing, generation pipeline
│ ├── llm.ts ← OpenRouter streaming client + system prompt
│ ├── sanitize.ts ← HTML allowlist + extracts link-hint metadata
│ ├── hints.ts ← D1 read/write for cross-article canon
│ ├── identity.ts ← LLM call that hallucinates {name, username}
│ ├── comments.ts ← Hono sub-app: threaded comments + voting + cookies
│ ├── ratelimit.ts ← Per-IP fixed-window KV limiter
│ ├── slug.ts ← Slug normalisation + reserved-slug list
│ ├── seed.ts ← Curated seed entries for the homepage
│ └── env.d.ts ← Worker env type
├── client/
│ ├── App.tsx ← SPA shell, history routing, streaming reader
│ ├── Comments.tsx ← Threaded HN-style comment UI
│ ├── AllEntries.tsx ← A–Z register of every article ever cached
│ └── styles.css ← Single hand-rolled stylesheet (parchment aesthetic)
├── shared/
│ └── … ← Types shared between worker & client
└── ...
migrations/
├── 0001_init.sql ← users, comments, votes
└── 0002_link_hints.sql ← (target_slug, source_slug) → blurb

Stack:

  • Cloudflare Workers — execution, runs everywhere, free tier covers viral.
  • Cloudflare KV — article HTML cache, stores {title, generatedAt} in metadata.
  • Cloudflare D1 — comments, users, votes, link hints.
  • Hono — small router + cookie helpers.
  • OpenRouter — LLM access (model is configurable via env var).
  • Vite + React 18 — SPA, no router library; history API by hand.
  • No build step on the worker. Wrangler bundles src/worker/index.ts and serves the Vite output as static assets via the ASSETS binding.

Local development

You will need: Node 20+, pnpm 9, and a Cloudflare account.

pnpm install
# Create a D1 database (one-time)
pnpm wrangler d1 create hallupedia
# Copy the printed database_id into wrangler.toml, replacing the placeholder.# Apply migrations locally
pnpm wrangler d1 migrations apply hallupedia --local
# Run vite (client) + wrangler (worker) concurrently
pnpm dev

Open http://localhost:8787. Articles will be generated on demand if you set your OPENROUTER_API_KEY (see below); otherwise the homepage seed will display but new entries will fail.

You can hit http://localhost:8787/api/index?refresh=1 at any time to force the total-entries counter to recount the KV namespace.

Deploying your own instance

# 1. Configure secrets
pnpm wrangler secret put OPENROUTER_API_KEY
# 2. Apply migrations to the remote D1
pnpm wrangler d1 migrations apply hallupedia --remote
# 3. Deploy
pnpm run deploy

The Worker handles its own routing, including /robots.txt, the SPA shell, and the API. If you bind a custom domain, edit the [[routes]] block in wrangler.toml. To deploy to a *.workers.dev URL instead, set workers_dev = true and remove the routes.

Configuration

Defined in wrangler.toml under [vars]:

VarDefaultPurpose
OPENROUTER_MODEL(set in toml)Model slug used for both article gen and identity hallucination
MAX_ARTICLES_PER_DAY5000Global circuit breaker — soft cap per UTC day
GEN_PER_IP_PER_HOUR100Per-IP article generation budget
IDENT_PER_IP_PER_HOUR10Per-IP cap on minting new commenter identities

Secrets (set via wrangler secret put):

SecretPurpose
OPENROUTER_API_KEYAuth for OpenRouter

Bindings:

  • ARTICLES — KV namespace for article HTML.
  • DB — D1 database for comments + link hints.
  • ASSETS — static assets (Vite build output).

Contributing

Pull requests welcome, especially anything that:

  • Reduces token spend per article without making the prose worse.
  • Improves cross-article consistency further.
  • Hardens the bot/UA defenses without breaking real readers.
  • Catches a "griffing" / prompt-injection vector you found in the wild.

Please open an issue first for anything user-facing so we can discuss tone — Halupedia lives or dies by its voice and an out-of-register entry is worse than no entry at all.

License

GPL-3.0. The source code in this repository is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

If the press has improved your day, you may buy us tokens or join the conversation on Discord.

About

Encyclopedia that hallucinates articles on the fly

Resources

Stars

244 stars

Watchers

4 watching

Forks

Sponsor this project

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Halupedia

"Comprehensive coverage of topics mainstream encyclopedias overlooked."

An infinite, hallucinated encyclopedia. Every link leads to an entry that does not exist yet — until you click it, at which point an LLM pretends it has always existed and writes it for you, in the deadpan register of a 19th-century scholarly press.

Live at halupedia.com. Cooked on a Cloudflare Worker. Cached forever in KV. Threaded HN-style comments under every article, no signup, AI-hallucinated identities. Patrons may buy us tokens so the press can keep printing. Editors and conspirators meet in the Discord.


Table of contents


What it is

Halupedia is a single-page Cloudflare Worker that:

  1. Serves a React SPA that looks like an old print encyclopedia.
  2. On a request for any unknown slug, calls an LLM (via OpenRouter) that returns an HTML article in the encyclopedia's voice — full of confident, plausible-sounding nonsense that is densely cross-linked to other entries that also do not yet exist.
  3. Caches that article in Cloudflare KV forever. Subsequent visits are free.
  4. Lets readers leave HN-style threaded comments without ever signing up. Names are hallucinated by the LLM on first comment and tied to a cookie.

There is no editorial staff, no truth, no warranty. Every article is invented on demand. The footnotes are also lies.

Star history

Star History Chart

How a page is born

 you click ─► /api/page/footnote-drift
│
▼
┌──────────────────┐
│ in KV already? │── yes ─► stream from KV (free)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ are you a bot? │── yes ─► 404 (no token spend)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ over IP rate? │── yes ─► 429 with Retry-After
└──────────────────┘
│ no
▼
┌──────────────────────────────────┐
│ load prior link-hints from D1 │
│ (canon set by other articles) │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ stream LLM via OpenRouter │
│ → split stream: │
│ a) sanitize + send to client │
│ b) collect, persist to KV, │
│ extract & save link hints │
└──────────────────────────────────┘

The HTML stream is split (ReadableStream.tee()) so the user starts reading the article while the worker is still receiving and persisting it. First paint is sub-second; the worker continues writing to KV under ctx.waitUntil() after the response closes.

Consistency in a hallucinated universe

The hardest problem with an infinite, on-demand encyclopedia is internal contradiction: article A says Mortimer Vellum died in 1843; article B, generated three weeks later, says he was alive in 1881. Halupedia solves this with link hints:

  • When the LLM writes an article, it is required to add a context="…" attribute on every <a> it inserts, summarising the future article it is linking to (e.g. context="19th-century clerk who formalized footnote drift, Pellbrick's mentor").
  • Before serving the HTML, the worker harvests these context values into a link_hints table in D1, keyed by (target_slug, source_slug).
  • The context attribute is stripped before the HTML is sent to the browser — readers never see the metadata.
  • When that target article is later requested for the first time, the worker loads the accumulated hints and injects them into the system prompt as "PRIOR REFERENCES — these are CANON". The LLM is instructed that the encyclopedia is hallucinated and absurd, but it must not contradict itself.

The result is a write-forward consistency mechanism: each article seeds breadcrumbs for the entries it links to, so by the time those entries are written, the LLM has a small dossier of established lore to honour.

Comments

Hacker-News-style threaded comments under every article. Backed by Cloudflare D1 (free tier). Notable behaviours:

  • No signup, ever. The first time you post, the LLM hallucinates a name and username for you (e.g. Bartram Pellbrick-Thwaite / pellbrick_archivist), in the same scholarly register as the rest of the site. You are inserted into D1 with a UUID and given a hu_uid cookie.
  • Cookie is effectively permanent (capped at 400 days per RFC 6265bis, refreshed on every authenticated request — so active users never expire).
  • One upvote per comment per user, toggleable. Optimistic UI.
  • Threaded to arbitrary depth, sorted by score DESC, created_at ASC.
  • Author auto-upvotes their own post, so every comment opens at score 1.
  • Per-IP rate limit on identity creation so a botnet can't burn your budget by minting fresh hallucinated names in a loop.

Defenses against runaway costs

LLM tokens cost real money and Halupedia is run by one person who lacks a corporate Amex. The worker has a layered defense:

LayerCatchesImplementation
1. User-Agent regexHonest crawlers (Googlebot, GPTBot, ClaudeBot, curl, wget, scrapy, …)src/worker/index.tsisBot()
2. Per-IP article gen budgetUA-forging scrapers, runaway tabsKV-backed fixed-window limiter, GEN_PER_IP_PER_HOUR
3. Per-IP identity-mint budgetCookie-rotating spammers minting hallucinated namesIDENT_PER_IP_PER_HOUR
4. Global daily capDistributed botnets that defeat 1–3MAX_ARTICLES_PER_DAY, KV counter
5. Cache foreverSame slug never costs twiceKV put() with metadata
6. Tee-and-persistStream interruptions don't waste a generationReadableStream.tee() + waitUntil()
7. Cloudflare dashboardVolumetric / L7 attacksWAF rate-limit + Bot Fight Mode

Crucially: cached articles are served to everyone, including bots. The bot guard only fires on uncached slugs, so anything you've already paid to generate stays freely indexable for SEO.

Architecture

src/
├── worker/
│ ├── index.ts ← Hono app, request routing, generation pipeline
│ ├── llm.ts ← OpenRouter streaming client + system prompt
│ ├── sanitize.ts ← HTML allowlist + extracts link-hint metadata
│ ├── hints.ts ← D1 read/write for cross-article canon
│ ├── identity.ts ← LLM call that hallucinates {name, username}
│ ├── comments.ts ← Hono sub-app: threaded comments + voting + cookies
│ ├── ratelimit.ts ← Per-IP fixed-window KV limiter
│ ├── slug.ts ← Slug normalisation + reserved-slug list
│ ├── seed.ts ← Curated seed entries for the homepage
│ └── env.d.ts ← Worker env type
├── client/
│ ├── App.tsx ← SPA shell, history routing, streaming reader
│ ├── Comments.tsx ← Threaded HN-style comment UI
│ ├── AllEntries.tsx ← A–Z register of every article ever cached
│ └── styles.css ← Single hand-rolled stylesheet (parchment aesthetic)
├── shared/
│ └── … ← Types shared between worker & client
└── ...
migrations/
├── 0001_init.sql ← users, comments, votes
└── 0002_link_hints.sql ← (target_slug, source_slug) → blurb

Stack:

  • Cloudflare Workers — execution, runs everywhere, free tier covers viral.
  • Cloudflare KV — article HTML cache, stores {title, generatedAt} in metadata.
  • Cloudflare D1 — comments, users, votes, link hints.
  • Hono — small router + cookie helpers.
  • OpenRouter — LLM access (model is configurable via env var).
  • Vite + React 18 — SPA, no router library; history API by hand.
  • No build step on the worker. Wrangler bundles src/worker/index.ts and serves the Vite output as static assets via the ASSETS binding.

Local development

You will need: Node 20+, pnpm 9, and a Cloudflare account.

pnpm install
# Create a D1 database (one-time)
pnpm wrangler d1 create hallupedia
# Copy the printed database_id into wrangler.toml, replacing the placeholder.# Apply migrations locally
pnpm wrangler d1 migrations apply hallupedia --local
# Run vite (client) + wrangler (worker) concurrently
pnpm dev

Open http://localhost:8787. Articles will be generated on demand if you set your OPENROUTER_API_KEY (see below); otherwise the homepage seed will display but new entries will fail.

You can hit http://localhost:8787/api/index?refresh=1 at any time to force the total-entries counter to recount the KV namespace.

Deploying your own instance

# 1. Configure secrets
pnpm wrangler secret put OPENROUTER_API_KEY
# 2. Apply migrations to the remote D1
pnpm wrangler d1 migrations apply hallupedia --remote
# 3. Deploy
pnpm run deploy

The Worker handles its own routing, including /robots.txt, the SPA shell, and the API. If you bind a custom domain, edit the [[routes]] block in wrangler.toml. To deploy to a *.workers.dev URL instead, set workers_dev = true and remove the routes.

Configuration

Defined in wrangler.toml under [vars]:

VarDefaultPurpose
OPENROUTER_MODEL(set in toml)Model slug used for both article gen and identity hallucination
MAX_ARTICLES_PER_DAY5000Global circuit breaker — soft cap per UTC day
GEN_PER_IP_PER_HOUR100Per-IP article generation budget
IDENT_PER_IP_PER_HOUR10Per-IP cap on minting new commenter identities

Secrets (set via wrangler secret put):

SecretPurpose
OPENROUTER_API_KEYAuth for OpenRouter

Bindings:

  • ARTICLES — KV namespace for article HTML.
  • DB — D1 database for comments + link hints.
  • ASSETS — static assets (Vite build output).

Contributing

Pull requests welcome, especially anything that:

  • Reduces token spend per article without making the prose worse.
  • Improves cross-article consistency further.
  • Hardens the bot/UA defenses without breaking real readers.
  • Catches a "griffing" / prompt-injection vector you found in the wild.

Please open an issue first for anything user-facing so we can discuss tone — Halupedia lives or dies by its voice and an out-of-register entry is worse than no entry at all.

License

GPL-3.0. The source code in this repository is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

If the press has improved your day, you may buy us tokens or join the conversation on Discord.

About

Encyclopedia that hallucinates articles on the fly

Resources

Stars

244 stars

Watchers

4 watching

Forks

Sponsor this project

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Halupedia

"Comprehensive coverage of topics mainstream encyclopedias overlooked."

An infinite, hallucinated encyclopedia. Every link leads to an entry that does not exist yet — until you click it, at which point an LLM pretends it has always existed and writes it for you, in the deadpan register of a 19th-century scholarly press.

Live at halupedia.com. Cooked on a Cloudflare Worker. Cached forever in KV. Threaded HN-style comments under every article, no signup, AI-hallucinated identities. Patrons may buy us tokens so the press can keep printing. Editors and conspirators meet in the Discord.


Table of contents


What it is

Halupedia is a single-page Cloudflare Worker that:

  1. Serves a React SPA that looks like an old print encyclopedia.
  2. On a request for any unknown slug, calls an LLM (via OpenRouter) that returns an HTML article in the encyclopedia's voice — full of confident, plausible-sounding nonsense that is densely cross-linked to other entries that also do not yet exist.
  3. Caches that article in Cloudflare KV forever. Subsequent visits are free.
  4. Lets readers leave HN-style threaded comments without ever signing up. Names are hallucinated by the LLM on first comment and tied to a cookie.

There is no editorial staff, no truth, no warranty. Every article is invented on demand. The footnotes are also lies.

Star history

Star History Chart

How a page is born

 you click ─► /api/page/footnote-drift
│
▼
┌──────────────────┐
│ in KV already? │── yes ─► stream from KV (free)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ are you a bot? │── yes ─► 404 (no token spend)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ over IP rate? │── yes ─► 429 with Retry-After
└──────────────────┘
│ no
▼
┌──────────────────────────────────┐
│ load prior link-hints from D1 │
│ (canon set by other articles) │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ stream LLM via OpenRouter │
│ → split stream: │
│ a) sanitize + send to client │
│ b) collect, persist to KV, │
│ extract & save link hints │
└──────────────────────────────────┘

The HTML stream is split (ReadableStream.tee()) so the user starts reading the article while the worker is still receiving and persisting it. First paint is sub-second; the worker continues writing to KV under ctx.waitUntil() after the response closes.

Consistency in a hallucinated universe

The hardest problem with an infinite, on-demand encyclopedia is internal contradiction: article A says Mortimer Vellum died in 1843; article B, generated three weeks later, says he was alive in 1881. Halupedia solves this with link hints:

  • When the LLM writes an article, it is required to add a context="…" attribute on every <a> it inserts, summarising the future article it is linking to (e.g. context="19th-century clerk who formalized footnote drift, Pellbrick's mentor").
  • Before serving the HTML, the worker harvests these context values into a link_hints table in D1, keyed by (target_slug, source_slug).
  • The context attribute is stripped before the HTML is sent to the browser — readers never see the metadata.
  • When that target article is later requested for the first time, the worker loads the accumulated hints and injects them into the system prompt as "PRIOR REFERENCES — these are CANON". The LLM is instructed that the encyclopedia is hallucinated and absurd, but it must not contradict itself.

The result is a write-forward consistency mechanism: each article seeds breadcrumbs for the entries it links to, so by the time those entries are written, the LLM has a small dossier of established lore to honour.

Comments

Hacker-News-style threaded comments under every article. Backed by Cloudflare D1 (free tier). Notable behaviours:

  • No signup, ever. The first time you post, the LLM hallucinates a name and username for you (e.g. Bartram Pellbrick-Thwaite / pellbrick_archivist), in the same scholarly register as the rest of the site. You are inserted into D1 with a UUID and given a hu_uid cookie.
  • Cookie is effectively permanent (capped at 400 days per RFC 6265bis, refreshed on every authenticated request — so active users never expire).
  • One upvote per comment per user, toggleable. Optimistic UI.
  • Threaded to arbitrary depth, sorted by score DESC, created_at ASC.
  • Author auto-upvotes their own post, so every comment opens at score 1.
  • Per-IP rate limit on identity creation so a botnet can't burn your budget by minting fresh hallucinated names in a loop.

Defenses against runaway costs

LLM tokens cost real money and Halupedia is run by one person who lacks a corporate Amex. The worker has a layered defense:

LayerCatchesImplementation
1. User-Agent regexHonest crawlers (Googlebot, GPTBot, ClaudeBot, curl, wget, scrapy, …)src/worker/index.tsisBot()
2. Per-IP article gen budgetUA-forging scrapers, runaway tabsKV-backed fixed-window limiter, GEN_PER_IP_PER_HOUR
3. Per-IP identity-mint budgetCookie-rotating spammers minting hallucinated namesIDENT_PER_IP_PER_HOUR
4. Global daily capDistributed botnets that defeat 1–3MAX_ARTICLES_PER_DAY, KV counter
5. Cache foreverSame slug never costs twiceKV put() with metadata
6. Tee-and-persistStream interruptions don't waste a generationReadableStream.tee() + waitUntil()
7. Cloudflare dashboardVolumetric / L7 attacksWAF rate-limit + Bot Fight Mode

Crucially: cached articles are served to everyone, including bots. The bot guard only fires on uncached slugs, so anything you've already paid to generate stays freely indexable for SEO.

Architecture

src/
├── worker/
│ ├── index.ts ← Hono app, request routing, generation pipeline
│ ├── llm.ts ← OpenRouter streaming client + system prompt
│ ├── sanitize.ts ← HTML allowlist + extracts link-hint metadata
│ ├── hints.ts ← D1 read/write for cross-article canon
│ ├── identity.ts ← LLM call that hallucinates {name, username}
│ ├── comments.ts ← Hono sub-app: threaded comments + voting + cookies
│ ├── ratelimit.ts ← Per-IP fixed-window KV limiter
│ ├── slug.ts ← Slug normalisation + reserved-slug list
│ ├── seed.ts ← Curated seed entries for the homepage
│ └── env.d.ts ← Worker env type
├── client/
│ ├── App.tsx ← SPA shell, history routing, streaming reader
│ ├── Comments.tsx ← Threaded HN-style comment UI
│ ├── AllEntries.tsx ← A–Z register of every article ever cached
│ └── styles.css ← Single hand-rolled stylesheet (parchment aesthetic)
├── shared/
│ └── … ← Types shared between worker & client
└── ...
migrations/
├── 0001_init.sql ← users, comments, votes
└── 0002_link_hints.sql ← (target_slug, source_slug) → blurb

Stack:

  • Cloudflare Workers — execution, runs everywhere, free tier covers viral.
  • Cloudflare KV — article HTML cache, stores {title, generatedAt} in metadata.
  • Cloudflare D1 — comments, users, votes, link hints.
  • Hono — small router + cookie helpers.
  • OpenRouter — LLM access (model is configurable via env var).
  • Vite + React 18 — SPA, no router library; history API by hand.
  • No build step on the worker. Wrangler bundles src/worker/index.ts and serves the Vite output as static assets via the ASSETS binding.

Local development

You will need: Node 20+, pnpm 9, and a Cloudflare account.

pnpm install
# Create a D1 database (one-time)
pnpm wrangler d1 create hallupedia
# Copy the printed database_id into wrangler.toml, replacing the placeholder.# Apply migrations locally
pnpm wrangler d1 migrations apply hallupedia --local
# Run vite (client) + wrangler (worker) concurrently
pnpm dev

Open http://localhost:8787. Articles will be generated on demand if you set your OPENROUTER_API_KEY (see below); otherwise the homepage seed will display but new entries will fail.

You can hit http://localhost:8787/api/index?refresh=1 at any time to force the total-entries counter to recount the KV namespace.

Deploying your own instance

# 1. Configure secrets
pnpm wrangler secret put OPENROUTER_API_KEY
# 2. Apply migrations to the remote D1
pnpm wrangler d1 migrations apply hallupedia --remote
# 3. Deploy
pnpm run deploy

The Worker handles its own routing, including /robots.txt, the SPA shell, and the API. If you bind a custom domain, edit the [[routes]] block in wrangler.toml. To deploy to a *.workers.dev URL instead, set workers_dev = true and remove the routes.

Configuration

Defined in wrangler.toml under [vars]:

VarDefaultPurpose
OPENROUTER_MODEL(set in toml)Model slug used for both article gen and identity hallucination
MAX_ARTICLES_PER_DAY5000Global circuit breaker — soft cap per UTC day
GEN_PER_IP_PER_HOUR100Per-IP article generation budget
IDENT_PER_IP_PER_HOUR10Per-IP cap on minting new commenter identities

Secrets (set via wrangler secret put):

SecretPurpose
OPENROUTER_API_KEYAuth for OpenRouter

Bindings:

  • ARTICLES — KV namespace for article HTML.
  • DB — D1 database for comments + link hints.
  • ASSETS — static assets (Vite build output).

Contributing

Pull requests welcome, especially anything that:

  • Reduces token spend per article without making the prose worse.
  • Improves cross-article consistency further.
  • Hardens the bot/UA defenses without breaking real readers.
  • Catches a "griffing" / prompt-injection vector you found in the wild.

Please open an issue first for anything user-facing so we can discuss tone — Halupedia lives or dies by its voice and an out-of-register entry is worse than no entry at all.

License

GPL-3.0. The source code in this repository is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

If the press has improved your day, you may buy us tokens or join the conversation on Discord.

About

Encyclopedia that hallucinates articles on the fly

Resources

Stars

244 stars

Watchers

4 watching

Forks

Sponsor this project

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Halupedia

"Comprehensive coverage of topics mainstream encyclopedias overlooked."

An infinite, hallucinated encyclopedia. Every link leads to an entry that does not exist yet — until you click it, at which point an LLM pretends it has always existed and writes it for you, in the deadpan register of a 19th-century scholarly press.

Live at halupedia.com. Cooked on a Cloudflare Worker. Cached forever in KV. Threaded HN-style comments under every article, no signup, AI-hallucinated identities. Patrons may buy us tokens so the press can keep printing. Editors and conspirators meet in the Discord.


Table of contents


What it is

Halupedia is a single-page Cloudflare Worker that:

  1. Serves a React SPA that looks like an old print encyclopedia.
  2. On a request for any unknown slug, calls an LLM (via OpenRouter) that returns an HTML article in the encyclopedia's voice — full of confident, plausible-sounding nonsense that is densely cross-linked to other entries that also do not yet exist.
  3. Caches that article in Cloudflare KV forever. Subsequent visits are free.
  4. Lets readers leave HN-style threaded comments without ever signing up. Names are hallucinated by the LLM on first comment and tied to a cookie.

There is no editorial staff, no truth, no warranty. Every article is invented on demand. The footnotes are also lies.

Star history

Star History Chart

How a page is born

 you click ─► /api/page/footnote-drift
│
▼
┌──────────────────┐
│ in KV already? │── yes ─► stream from KV (free)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ are you a bot? │── yes ─► 404 (no token spend)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ over IP rate? │── yes ─► 429 with Retry-After
└──────────────────┘
│ no
▼
┌──────────────────────────────────┐
│ load prior link-hints from D1 │
│ (canon set by other articles) │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ stream LLM via OpenRouter │
│ → split stream: │
│ a) sanitize + send to client │
│ b) collect, persist to KV, │
│ extract & save link hints │
└──────────────────────────────────┘

The HTML stream is split (ReadableStream.tee()) so the user starts reading the article while the worker is still receiving and persisting it. First paint is sub-second; the worker continues writing to KV under ctx.waitUntil() after the response closes.

Consistency in a hallucinated universe

The hardest problem with an infinite, on-demand encyclopedia is internal contradiction: article A says Mortimer Vellum died in 1843; article B, generated three weeks later, says he was alive in 1881. Halupedia solves this with link hints:

  • When the LLM writes an article, it is required to add a context="…" attribute on every <a> it inserts, summarising the future article it is linking to (e.g. context="19th-century clerk who formalized footnote drift, Pellbrick's mentor").
  • Before serving the HTML, the worker harvests these context values into a link_hints table in D1, keyed by (target_slug, source_slug).
  • The context attribute is stripped before the HTML is sent to the browser — readers never see the metadata.
  • When that target article is later requested for the first time, the worker loads the accumulated hints and injects them into the system prompt as "PRIOR REFERENCES — these are CANON". The LLM is instructed that the encyclopedia is hallucinated and absurd, but it must not contradict itself.

The result is a write-forward consistency mechanism: each article seeds breadcrumbs for the entries it links to, so by the time those entries are written, the LLM has a small dossier of established lore to honour.

Comments

Hacker-News-style threaded comments under every article. Backed by Cloudflare D1 (free tier). Notable behaviours:

  • No signup, ever. The first time you post, the LLM hallucinates a name and username for you (e.g. Bartram Pellbrick-Thwaite / pellbrick_archivist), in the same scholarly register as the rest of the site. You are inserted into D1 with a UUID and given a hu_uid cookie.
  • Cookie is effectively permanent (capped at 400 days per RFC 6265bis, refreshed on every authenticated request — so active users never expire).
  • One upvote per comment per user, toggleable. Optimistic UI.
  • Threaded to arbitrary depth, sorted by score DESC, created_at ASC.
  • Author auto-upvotes their own post, so every comment opens at score 1.
  • Per-IP rate limit on identity creation so a botnet can't burn your budget by minting fresh hallucinated names in a loop.

Defenses against runaway costs

LLM tokens cost real money and Halupedia is run by one person who lacks a corporate Amex. The worker has a layered defense:

LayerCatchesImplementation
1. User-Agent regexHonest crawlers (Googlebot, GPTBot, ClaudeBot, curl, wget, scrapy, …)src/worker/index.tsisBot()
2. Per-IP article gen budgetUA-forging scrapers, runaway tabsKV-backed fixed-window limiter, GEN_PER_IP_PER_HOUR
3. Per-IP identity-mint budgetCookie-rotating spammers minting hallucinated namesIDENT_PER_IP_PER_HOUR
4. Global daily capDistributed botnets that defeat 1–3MAX_ARTICLES_PER_DAY, KV counter
5. Cache foreverSame slug never costs twiceKV put() with metadata
6. Tee-and-persistStream interruptions don't waste a generationReadableStream.tee() + waitUntil()
7. Cloudflare dashboardVolumetric / L7 attacksWAF rate-limit + Bot Fight Mode

Crucially: cached articles are served to everyone, including bots. The bot guard only fires on uncached slugs, so anything you've already paid to generate stays freely indexable for SEO.

Architecture

src/
├── worker/
│ ├── index.ts ← Hono app, request routing, generation pipeline
│ ├── llm.ts ← OpenRouter streaming client + system prompt
│ ├── sanitize.ts ← HTML allowlist + extracts link-hint metadata
│ ├── hints.ts ← D1 read/write for cross-article canon
│ ├── identity.ts ← LLM call that hallucinates {name, username}
│ ├── comments.ts ← Hono sub-app: threaded comments + voting + cookies
│ ├── ratelimit.ts ← Per-IP fixed-window KV limiter
│ ├── slug.ts ← Slug normalisation + reserved-slug list
│ ├── seed.ts ← Curated seed entries for the homepage
│ └── env.d.ts ← Worker env type
├── client/
│ ├── App.tsx ← SPA shell, history routing, streaming reader
│ ├── Comments.tsx ← Threaded HN-style comment UI
│ ├── AllEntries.tsx ← A–Z register of every article ever cached
│ └── styles.css ← Single hand-rolled stylesheet (parchment aesthetic)
├── shared/
│ └── … ← Types shared between worker & client
└── ...
migrations/
├── 0001_init.sql ← users, comments, votes
└── 0002_link_hints.sql ← (target_slug, source_slug) → blurb

Stack:

  • Cloudflare Workers — execution, runs everywhere, free tier covers viral.
  • Cloudflare KV — article HTML cache, stores {title, generatedAt} in metadata.
  • Cloudflare D1 — comments, users, votes, link hints.
  • Hono — small router + cookie helpers.
  • OpenRouter — LLM access (model is configurable via env var).
  • Vite + React 18 — SPA, no router library; history API by hand.
  • No build step on the worker. Wrangler bundles src/worker/index.ts and serves the Vite output as static assets via the ASSETS binding.

Local development

You will need: Node 20+, pnpm 9, and a Cloudflare account.

pnpm install
# Create a D1 database (one-time)
pnpm wrangler d1 create hallupedia
# Copy the printed database_id into wrangler.toml, replacing the placeholder.# Apply migrations locally
pnpm wrangler d1 migrations apply hallupedia --local
# Run vite (client) + wrangler (worker) concurrently
pnpm dev

Open http://localhost:8787. Articles will be generated on demand if you set your OPENROUTER_API_KEY (see below); otherwise the homepage seed will display but new entries will fail.

You can hit http://localhost:8787/api/index?refresh=1 at any time to force the total-entries counter to recount the KV namespace.

Deploying your own instance

# 1. Configure secrets
pnpm wrangler secret put OPENROUTER_API_KEY
# 2. Apply migrations to the remote D1
pnpm wrangler d1 migrations apply hallupedia --remote
# 3. Deploy
pnpm run deploy

The Worker handles its own routing, including /robots.txt, the SPA shell, and the API. If you bind a custom domain, edit the [[routes]] block in wrangler.toml. To deploy to a *.workers.dev URL instead, set workers_dev = true and remove the routes.

Configuration

Defined in wrangler.toml under [vars]:

VarDefaultPurpose
OPENROUTER_MODEL(set in toml)Model slug used for both article gen and identity hallucination
MAX_ARTICLES_PER_DAY5000Global circuit breaker — soft cap per UTC day
GEN_PER_IP_PER_HOUR100Per-IP article generation budget
IDENT_PER_IP_PER_HOUR10Per-IP cap on minting new commenter identities

Secrets (set via wrangler secret put):

SecretPurpose
OPENROUTER_API_KEYAuth for OpenRouter

Bindings:

  • ARTICLES — KV namespace for article HTML.
  • DB — D1 database for comments + link hints.
  • ASSETS — static assets (Vite build output).

Contributing

Pull requests welcome, especially anything that:

  • Reduces token spend per article without making the prose worse.
  • Improves cross-article consistency further.
  • Hardens the bot/UA defenses without breaking real readers.
  • Catches a "griffing" / prompt-injection vector you found in the wild.

Please open an issue first for anything user-facing so we can discuss tone — Halupedia lives or dies by its voice and an out-of-register entry is worse than no entry at all.

License

GPL-3.0. The source code in this repository is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

If the press has improved your day, you may buy us tokens or join the conversation on Discord.

About

Encyclopedia that hallucinates articles on the fly

Resources

Stars

244 stars

Watchers

4 watching

Forks

Sponsor this project

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Halupedia

"Comprehensive coverage of topics mainstream encyclopedias overlooked."

An infinite, hallucinated encyclopedia. Every link leads to an entry that does not exist yet — until you click it, at which point an LLM pretends it has always existed and writes it for you, in the deadpan register of a 19th-century scholarly press.

Live at halupedia.com. Cooked on a Cloudflare Worker. Cached forever in KV. Threaded HN-style comments under every article, no signup, AI-hallucinated identities. Patrons may buy us tokens so the press can keep printing. Editors and conspirators meet in the Discord.


Table of contents


What it is

Halupedia is a single-page Cloudflare Worker that:

  1. Serves a React SPA that looks like an old print encyclopedia.
  2. On a request for any unknown slug, calls an LLM (via OpenRouter) that returns an HTML article in the encyclopedia's voice — full of confident, plausible-sounding nonsense that is densely cross-linked to other entries that also do not yet exist.
  3. Caches that article in Cloudflare KV forever. Subsequent visits are free.
  4. Lets readers leave HN-style threaded comments without ever signing up. Names are hallucinated by the LLM on first comment and tied to a cookie.

There is no editorial staff, no truth, no warranty. Every article is invented on demand. The footnotes are also lies.

Star history

Star History Chart

How a page is born

 you click ─► /api/page/footnote-drift
│
▼
┌──────────────────┐
│ in KV already? │── yes ─► stream from KV (free)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ are you a bot? │── yes ─► 404 (no token spend)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ over IP rate? │── yes ─► 429 with Retry-After
└──────────────────┘
│ no
▼
┌──────────────────────────────────┐
│ load prior link-hints from D1 │
│ (canon set by other articles) │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ stream LLM via OpenRouter │
│ → split stream: │
│ a) sanitize + send to client │
│ b) collect, persist to KV, │
│ extract & save link hints │
└──────────────────────────────────┘

The HTML stream is split (ReadableStream.tee()) so the user starts reading the article while the worker is still receiving and persisting it. First paint is sub-second; the worker continues writing to KV under ctx.waitUntil() after the response closes.

Consistency in a hallucinated universe

The hardest problem with an infinite, on-demand encyclopedia is internal contradiction: article A says Mortimer Vellum died in 1843; article B, generated three weeks later, says he was alive in 1881. Halupedia solves this with link hints:

  • When the LLM writes an article, it is required to add a context="…" attribute on every <a> it inserts, summarising the future article it is linking to (e.g. context="19th-century clerk who formalized footnote drift, Pellbrick's mentor").
  • Before serving the HTML, the worker harvests these context values into a link_hints table in D1, keyed by (target_slug, source_slug).
  • The context attribute is stripped before the HTML is sent to the browser — readers never see the metadata.
  • When that target article is later requested for the first time, the worker loads the accumulated hints and injects them into the system prompt as "PRIOR REFERENCES — these are CANON". The LLM is instructed that the encyclopedia is hallucinated and absurd, but it must not contradict itself.

The result is a write-forward consistency mechanism: each article seeds breadcrumbs for the entries it links to, so by the time those entries are written, the LLM has a small dossier of established lore to honour.

Comments

Hacker-News-style threaded comments under every article. Backed by Cloudflare D1 (free tier). Notable behaviours:

  • No signup, ever. The first time you post, the LLM hallucinates a name and username for you (e.g. Bartram Pellbrick-Thwaite / pellbrick_archivist), in the same scholarly register as the rest of the site. You are inserted into D1 with a UUID and given a hu_uid cookie.
  • Cookie is effectively permanent (capped at 400 days per RFC 6265bis, refreshed on every authenticated request — so active users never expire).
  • One upvote per comment per user, toggleable. Optimistic UI.
  • Threaded to arbitrary depth, sorted by score DESC, created_at ASC.
  • Author auto-upvotes their own post, so every comment opens at score 1.
  • Per-IP rate limit on identity creation so a botnet can't burn your budget by minting fresh hallucinated names in a loop.

Defenses against runaway costs

LLM tokens cost real money and Halupedia is run by one person who lacks a corporate Amex. The worker has a layered defense:

LayerCatchesImplementation
1. User-Agent regexHonest crawlers (Googlebot, GPTBot, ClaudeBot, curl, wget, scrapy, …)src/worker/index.tsisBot()
2. Per-IP article gen budgetUA-forging scrapers, runaway tabsKV-backed fixed-window limiter, GEN_PER_IP_PER_HOUR
3. Per-IP identity-mint budgetCookie-rotating spammers minting hallucinated namesIDENT_PER_IP_PER_HOUR
4. Global daily capDistributed botnets that defeat 1–3MAX_ARTICLES_PER_DAY, KV counter
5. Cache foreverSame slug never costs twiceKV put() with metadata
6. Tee-and-persistStream interruptions don't waste a generationReadableStream.tee() + waitUntil()
7. Cloudflare dashboardVolumetric / L7 attacksWAF rate-limit + Bot Fight Mode

Crucially: cached articles are served to everyone, including bots. The bot guard only fires on uncached slugs, so anything you've already paid to generate stays freely indexable for SEO.

Architecture

src/
├── worker/
│ ├── index.ts ← Hono app, request routing, generation pipeline
│ ├── llm.ts ← OpenRouter streaming client + system prompt
│ ├── sanitize.ts ← HTML allowlist + extracts link-hint metadata
│ ├── hints.ts ← D1 read/write for cross-article canon
│ ├── identity.ts ← LLM call that hallucinates {name, username}
│ ├── comments.ts ← Hono sub-app: threaded comments + voting + cookies
│ ├── ratelimit.ts ← Per-IP fixed-window KV limiter
│ ├── slug.ts ← Slug normalisation + reserved-slug list
│ ├── seed.ts ← Curated seed entries for the homepage
│ └── env.d.ts ← Worker env type
├── client/
│ ├── App.tsx ← SPA shell, history routing, streaming reader
│ ├── Comments.tsx ← Threaded HN-style comment UI
│ ├── AllEntries.tsx ← A–Z register of every article ever cached
│ └── styles.css ← Single hand-rolled stylesheet (parchment aesthetic)
├── shared/
│ └── … ← Types shared between worker & client
└── ...
migrations/
├── 0001_init.sql ← users, comments, votes
└── 0002_link_hints.sql ← (target_slug, source_slug) → blurb

Stack:

  • Cloudflare Workers — execution, runs everywhere, free tier covers viral.
  • Cloudflare KV — article HTML cache, stores {title, generatedAt} in metadata.
  • Cloudflare D1 — comments, users, votes, link hints.
  • Hono — small router + cookie helpers.
  • OpenRouter — LLM access (model is configurable via env var).
  • Vite + React 18 — SPA, no router library; history API by hand.
  • No build step on the worker. Wrangler bundles src/worker/index.ts and serves the Vite output as static assets via the ASSETS binding.

Local development

You will need: Node 20+, pnpm 9, and a Cloudflare account.

pnpm install
# Create a D1 database (one-time)
pnpm wrangler d1 create hallupedia
# Copy the printed database_id into wrangler.toml, replacing the placeholder.# Apply migrations locally
pnpm wrangler d1 migrations apply hallupedia --local
# Run vite (client) + wrangler (worker) concurrently
pnpm dev

Open http://localhost:8787. Articles will be generated on demand if you set your OPENROUTER_API_KEY (see below); otherwise the homepage seed will display but new entries will fail.

You can hit http://localhost:8787/api/index?refresh=1 at any time to force the total-entries counter to recount the KV namespace.

Deploying your own instance

# 1. Configure secrets
pnpm wrangler secret put OPENROUTER_API_KEY
# 2. Apply migrations to the remote D1
pnpm wrangler d1 migrations apply hallupedia --remote
# 3. Deploy
pnpm run deploy

The Worker handles its own routing, including /robots.txt, the SPA shell, and the API. If you bind a custom domain, edit the [[routes]] block in wrangler.toml. To deploy to a *.workers.dev URL instead, set workers_dev = true and remove the routes.

Configuration

Defined in wrangler.toml under [vars]:

VarDefaultPurpose
OPENROUTER_MODEL(set in toml)Model slug used for both article gen and identity hallucination
MAX_ARTICLES_PER_DAY5000Global circuit breaker — soft cap per UTC day
GEN_PER_IP_PER_HOUR100Per-IP article generation budget
IDENT_PER_IP_PER_HOUR10Per-IP cap on minting new commenter identities

Secrets (set via wrangler secret put):

SecretPurpose
OPENROUTER_API_KEYAuth for OpenRouter

Bindings:

  • ARTICLES — KV namespace for article HTML.
  • DB — D1 database for comments + link hints.
  • ASSETS — static assets (Vite build output).

Contributing

Pull requests welcome, especially anything that:

  • Reduces token spend per article without making the prose worse.
  • Improves cross-article consistency further.
  • Hardens the bot/UA defenses without breaking real readers.
  • Catches a "griffing" / prompt-injection vector you found in the wild.

Please open an issue first for anything user-facing so we can discuss tone — Halupedia lives or dies by its voice and an out-of-register entry is worse than no entry at all.

License

GPL-3.0. The source code in this repository is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

If the press has improved your day, you may buy us tokens or join the conversation on Discord.

About

Encyclopedia that hallucinates articles on the fly

Resources

Stars

244 stars

Watchers

4 watching

Forks

Sponsor this project

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Halupedia

"Comprehensive coverage of topics mainstream encyclopedias overlooked."

An infinite, hallucinated encyclopedia. Every link leads to an entry that does not exist yet — until you click it, at which point an LLM pretends it has always existed and writes it for you, in the deadpan register of a 19th-century scholarly press.

Live at halupedia.com. Cooked on a Cloudflare Worker. Cached forever in KV. Threaded HN-style comments under every article, no signup, AI-hallucinated identities. Patrons may buy us tokens so the press can keep printing. Editors and conspirators meet in the Discord.


Table of contents


What it is

Halupedia is a single-page Cloudflare Worker that:

  1. Serves a React SPA that looks like an old print encyclopedia.
  2. On a request for any unknown slug, calls an LLM (via OpenRouter) that returns an HTML article in the encyclopedia's voice — full of confident, plausible-sounding nonsense that is densely cross-linked to other entries that also do not yet exist.
  3. Caches that article in Cloudflare KV forever. Subsequent visits are free.
  4. Lets readers leave HN-style threaded comments without ever signing up. Names are hallucinated by the LLM on first comment and tied to a cookie.

There is no editorial staff, no truth, no warranty. Every article is invented on demand. The footnotes are also lies.

Star history

Star History Chart

How a page is born

 you click ─► /api/page/footnote-drift
│
▼
┌──────────────────┐
│ in KV already? │── yes ─► stream from KV (free)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ are you a bot? │── yes ─► 404 (no token spend)
└──────────────────┘
│ no
▼
┌──────────────────┐
│ over IP rate? │── yes ─► 429 with Retry-After
└──────────────────┘
│ no
▼
┌──────────────────────────────────┐
│ load prior link-hints from D1 │
│ (canon set by other articles) │
└──────────────────────────────────┘
│
▼
┌──────────────────────────────────┐
│ stream LLM via OpenRouter │
│ → split stream: │
│ a) sanitize + send to client │
│ b) collect, persist to KV, │
│ extract & save link hints │
└──────────────────────────────────┘

The HTML stream is split (ReadableStream.tee()) so the user starts reading the article while the worker is still receiving and persisting it. First paint is sub-second; the worker continues writing to KV under ctx.waitUntil() after the response closes.

Consistency in a hallucinated universe

The hardest problem with an infinite, on-demand encyclopedia is internal contradiction: article A says Mortimer Vellum died in 1843; article B, generated three weeks later, says he was alive in 1881. Halupedia solves this with link hints:

  • When the LLM writes an article, it is required to add a context="…" attribute on every <a> it inserts, summarising the future article it is linking to (e.g. context="19th-century clerk who formalized footnote drift, Pellbrick's mentor").
  • Before serving the HTML, the worker harvests these context values into a link_hints table in D1, keyed by (target_slug, source_slug).
  • The context attribute is stripped before the HTML is sent to the browser — readers never see the metadata.
  • When that target article is later requested for the first time, the worker loads the accumulated hints and injects them into the system prompt as "PRIOR REFERENCES — these are CANON". The LLM is instructed that the encyclopedia is hallucinated and absurd, but it must not contradict itself.

The result is a write-forward consistency mechanism: each article seeds breadcrumbs for the entries it links to, so by the time those entries are written, the LLM has a small dossier of established lore to honour.

Comments

Hacker-News-style threaded comments under every article. Backed by Cloudflare D1 (free tier). Notable behaviours:

  • No signup, ever. The first time you post, the LLM hallucinates a name and username for you (e.g. Bartram Pellbrick-Thwaite / pellbrick_archivist), in the same scholarly register as the rest of the site. You are inserted into D1 with a UUID and given a hu_uid cookie.
  • Cookie is effectively permanent (capped at 400 days per RFC 6265bis, refreshed on every authenticated request — so active users never expire).
  • One upvote per comment per user, toggleable. Optimistic UI.
  • Threaded to arbitrary depth, sorted by score DESC, created_at ASC.
  • Author auto-upvotes their own post, so every comment opens at score 1.
  • Per-IP rate limit on identity creation so a botnet can't burn your budget by minting fresh hallucinated names in a loop.

Defenses against runaway costs

LLM tokens cost real money and Halupedia is run by one person who lacks a corporate Amex. The worker has a layered defense:

LayerCatchesImplementation
1. User-Agent regexHonest crawlers (Googlebot, GPTBot, ClaudeBot, curl, wget, scrapy, …)src/worker/index.tsisBot()
2. Per-IP article gen budgetUA-forging scrapers, runaway tabsKV-backed fixed-window limiter, GEN_PER_IP_PER_HOUR
3. Per-IP identity-mint budgetCookie-rotating spammers minting hallucinated namesIDENT_PER_IP_PER_HOUR
4. Global daily capDistributed botnets that defeat 1–3MAX_ARTICLES_PER_DAY, KV counter
5. Cache foreverSame slug never costs twiceKV put() with metadata
6. Tee-and-persistStream interruptions don't waste a generationReadableStream.tee() + waitUntil()
7. Cloudflare dashboardVolumetric / L7 attacksWAF rate-limit + Bot Fight Mode

Crucially: cached articles are served to everyone, including bots. The bot guard only fires on uncached slugs, so anything you've already paid to generate stays freely indexable for SEO.

Architecture

src/
├── worker/
│ ├── index.ts ← Hono app, request routing, generation pipeline
│ ├── llm.ts ← OpenRouter streaming client + system prompt
│ ├── sanitize.ts ← HTML allowlist + extracts link-hint metadata
│ ├── hints.ts ← D1 read/write for cross-article canon
│ ├── identity.ts ← LLM call that hallucinates {name, username}
│ ├── comments.ts ← Hono sub-app: threaded comments + voting + cookies
│ ├── ratelimit.ts ← Per-IP fixed-window KV limiter
│ ├── slug.ts ← Slug normalisation + reserved-slug list
│ ├── seed.ts ← Curated seed entries for the homepage
│ └── env.d.ts ← Worker env type
├── client/
│ ├── App.tsx ← SPA shell, history routing, streaming reader
│ ├── Comments.tsx ← Threaded HN-style comment UI
│ ├── AllEntries.tsx ← A–Z register of every article ever cached
│ └── styles.css ← Single hand-rolled stylesheet (parchment aesthetic)
├── shared/
│ └── … ← Types shared between worker & client
└── ...
migrations/
├── 0001_init.sql ← users, comments, votes
└── 0002_link_hints.sql ← (target_slug, source_slug) → blurb

Stack:

  • Cloudflare Workers — execution, runs everywhere, free tier covers viral.
  • Cloudflare KV — article HTML cache, stores {title, generatedAt} in metadata.
  • Cloudflare D1 — comments, users, votes, link hints.
  • Hono — small router + cookie helpers.
  • OpenRouter — LLM access (model is configurable via env var).
  • Vite + React 18 — SPA, no router library; history API by hand.
  • No build step on the worker. Wrangler bundles src/worker/index.ts and serves the Vite output as static assets via the ASSETS binding.

Local development

You will need: Node 20+, pnpm 9, and a Cloudflare account.

pnpm install
# Create a D1 database (one-time)
pnpm wrangler d1 create hallupedia
# Copy the printed database_id into wrangler.toml, replacing the placeholder.# Apply migrations locally
pnpm wrangler d1 migrations apply hallupedia --local
# Run vite (client) + wrangler (worker) concurrently
pnpm dev

Open http://localhost:8787. Articles will be generated on demand if you set your OPENROUTER_API_KEY (see below); otherwise the homepage seed will display but new entries will fail.

You can hit http://localhost:8787/api/index?refresh=1 at any time to force the total-entries counter to recount the KV namespace.

Deploying your own instance

# 1. Configure secrets
pnpm wrangler secret put OPENROUTER_API_KEY
# 2. Apply migrations to the remote D1
pnpm wrangler d1 migrations apply hallupedia --remote
# 3. Deploy
pnpm run deploy

The Worker handles its own routing, including /robots.txt, the SPA shell, and the API. If you bind a custom domain, edit the [[routes]] block in wrangler.toml. To deploy to a *.workers.dev URL instead, set workers_dev = true and remove the routes.

Configuration

Defined in wrangler.toml under [vars]:

VarDefaultPurpose
OPENROUTER_MODEL(set in toml)Model slug used for both article gen and identity hallucination
MAX_ARTICLES_PER_DAY5000Global circuit breaker — soft cap per UTC day
GEN_PER_IP_PER_HOUR100Per-IP article generation budget
IDENT_PER_IP_PER_HOUR10Per-IP cap on minting new commenter identities

Secrets (set via wrangler secret put):

SecretPurpose
OPENROUTER_API_KEYAuth for OpenRouter

Bindings:

  • ARTICLES — KV namespace for article HTML.
  • DB — D1 database for comments + link hints.
  • ASSETS — static assets (Vite build output).

Contributing

Pull requests welcome, especially anything that:

  • Reduces token spend per article without making the prose worse.
  • Improves cross-article consistency further.
  • Hardens the bot/UA defenses without breaking real readers.
  • Catches a "griffing" / prompt-injection vector you found in the wild.

Please open an issue first for anything user-facing so we can discuss tone — Halupedia lives or dies by its voice and an out-of-register entry is worse than no entry at all.

License

GPL-3.0. The source code in this repository is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

If the press has improved your day, you may buy us tokens or join the conversation on Discord.

About

Encyclopedia that hallucinates articles on the fly

Resources

Stars

244 stars

Watchers

4 watching

Forks

Sponsor this project

Contributors

Languages