This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

A Hacktoberfest 2026 submission for "Ghost in the Machine"

A digital séance for dead websites — with an invariant that cost me
happy and sad at the same time , lots lesson learned, and the broken CORS policy.

There's a very specific kind of grief that comes with losing a website.

Not the grief of losing a person — that's different, sharper. This is the slow ache of remembering a URL that used to mean something, and typing it into a browser that returns a blank parking page. Geocities. Friendster. Vine. SixDegrees. Boo.com and even MySpace? Every single one of them used to exist. They had users. They had servers. They had cooling fans that ran at 3 AM because someone, somewhere, was still scrolling.

And then they just... stopped. Not with a bang. With a redirect.

I built OSSUARY for that feeling.

But here's what took me the longest to get right — and the thing I want to lead with:

OSSUARY refuses to lie.

Most "haunted AI" projects work the same way: you point a model at a URL, and the model happily hallucinates a spooky story. Try dev.to and it'll tell you about "a defunct developer community that perished in a tragic server fire." dev.to is not defunct. dev.to is literally hosting the challenge I'm submitting this article to.

So before OSSUARY ever calls an LLM, it asks one question: is this domain actually dead?

Four independent signals have to agree — DNS, WHOIS, Wayback Machine, Wikipedia. If any of them contradicts, OSSUARY refuses with HTTP 422 ("still_breathing") and logs the refusal in Sentry with a tag kind: paranormal, system: gate. Every refusal is an event. Every refusal is part of the story.

That single decision — refusing to lie — turned a toy into something I actually believe in.

What I Built

OSSUARY is a digital séance for the internet's dead. You whisper a dead domain into the altar, and the ghost of that domain speaks back — a first-person narrative, generated by an open-weight model, voiced by AI, archived forever in a database.

Screenshot of the OSSUARY dark mode interface showing a digital séance for a dead domain

How it interprets "Ghost in the Machine": most horror-AI demos treat the machine as a source of fiction. I wanted the opposite — the machine as archivist. The ghost isn't invented. It's reconstructed from real data: the last packet the server ever sent, the last snapshot the Wayback Machine ever took, the last line Wikipedia ever wrote before the shutdown was finalized.

The four stages of a summon:

  1. Reconnaissance — DNS lookup, WHOIS via RDAP (with three-layer fallback), Wayback CDX for the last pre-death snapshot, Wikipedia for the death year. All four run in parallel with hard timeouts.
  2. Narrative Generation — A 5-tier LLM cascade (Gemma 4 31B → Google GenAI → OpenRouter → deterministic template). The user never sees a failed summon.
  3. Voice Synthesis — ElevenLabs, Adam voice, dominant and firm. The closest synthetic approximation of a tired bureaucrat who used to run a server.
  4. Persistence — Full profile in MongoDB Atlas, audio in GridFS, time-series event in TigerData, semantic memory in Backboard.

Every one of these systems can fail. When one does, it doesn't produce an error screen — it produces a diegetic message:

"The echo faded before it could be recorded." (Backboard timeout)

"The ghost has lost its voice." (ElevenLabs quota)

The refusal gate is enforced in three places in the codebase:

  • api/recon.ts — returns HTTP 422 for living domains
  • api/conjure.ts — checks footprint.ping.alive and refuses if true
  • api/wiki.ts — filters Wikipedia disambiguation pages so "Vine" doesn't resolve to a plant

Demo

🔗 Live séance:

OSSUARY: The Last Packet

Digital necromancy tool that summons the ghosts of dead domains and servers.

favicon ossuary.saturumah.net

🎬 Video walkthrough:

Try these in order:

Domain Expected behavior
dev.to Refuses with HTTP 422 — "still breathing"
pets.com Summons ghost from 2000 (bankruptcy year)
geocities.com Summons ghost from 2009 (Yahoo shutdown)
vine.co Summons ghost from 2017 (Twitter shutdown)
myspace.com Summons ghost from 2011 (sold to Specific Media)

The myspace.com case is my favorite edge case. The domain is still alive (it's a music portal now), but the social network you remember died in 2011. OSSUARY makes an editorial decision to treat repurposed domains as "dead services" — because the ghost we want to summon is the Myspace of 2005, not the music portal of 2026.

Code

📂 Repository:

GitHub logo setuju / ossuary

OSSUARY: digital necromancy for dead domains — Hacktoberfest 2026 Ghost in the Machine

◈ OSSUARY

The Last Packet

A digital necromancy ritual for the internet's dead.

Live Ritual · Demo · Report Bug · Request Feature


Vercel Render TypeScript License PRs Welcome

Hacktoberfest 2026 · Ghost in the Machine

"Every ghost-story project points a model at dead code. OSSUARY checks if it's dead first."

What is OSSUARY?

OSSUARY is a séance for the internet's forgotten corners. Whisper a domain that no longer exists — a dead startup, an abandoned fanpage, a service killed by an acquisition — and the ghost of that domain will speak back. It pulls its last heartbeat from the Wayback Machine, its obituary from Wikipedia, its echoes from SerpApi, and weaves them into a first-person narrative, voiced aloud by ElevenLabs and preserved forever in MongoDB Atlas.

But here's the twist that makes it more than a horror toy:

OSSUARY refuses to lie.

If the domain is still alive — if the…

Architecture

╔══════════════════════════════════════════════════════════════════════════╗ ║ ║ ║ ██████╗ ███████╗███████╗██╗ ██╗ █████╗ ██████╗ ██╗ ██╗ ║ ║ ██╔═══██╗██╔════╝██╔════╝██║ ██║██╔══██╗██╔══██╗╚██╗ ██╔╝ ║ ║ ██║ ██║███████╗███████╗██║ ██║███████║██████╔╝ ╚████╔╝ ║ ║ ██║ ██║╚════██║╚════██║██║ ██║██╔══██║██╔══██╗ ╚██╔╝ ║ ║ ╚██████╔╝███████║███████║╚██████╔╝██║ ██║██║ ██║ ██║ ║ ║ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ║ ║ ║ ║ ── THE LAST PACKET ── ║ ║ ║ ╚══════════════════════════════════════════════════════════════════════════╝ ▲ │ https://ossuary.saturumah.net │ ┌───────────────────────┴──────────────────────────────────────────────────┐ │ │ │ VERCEL · THE ALTAR (Frontend) │ │ ───────────────────────────── │ │ │ │ ┌────────────┐ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ React 19 │→→→| TypeScript │→→→│ Vite 8 │→→→│ Tailwind v4 │ │ │ └────────────┘ └──────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ [ Idle Altar ] → [ Boot Screen ] → [ Ghost Screen ] → [ Ledger ] │ │ │ │ │ │ │ │ └───────────────────┴──────────────────┴────────────────┘ │ │ │ └───────────────────────────────┬──────────────────────────────────────────┘ │ │ fetch( VITE_API_URL + '/api/*' ) │ ┌───────────────────────────────▼──────────────────────────────────────────┐ │ │ │ RENDER · THE NECROPOLIS (Backend) │ │ ───────────────────────────────── │ │ │ │ ┌────────────────────────────────────────────────────────────────────┐ │ │ │ server-production.ts (Express + tsx) │ │ │ └────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ /recon │ │ /conjure │ │ /wiki │ │ /echoes │ │ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ │ │ │ │ │ └─────────┼────────────────┼────────────────┼────────────────┼─────────────┘ │ │ │ │ │ ┌──────▼──────┐ │ │ │ │ REFUSE- │ │ │ │ │ TO-LIE │ │ │ │ │ GATE │ │ │ │ │ ───────── │ │ │ │ │ HTTP 422 │ │ │ │ │ still_ │ │ │ │ │ breathing │ │ │ │ └──────┬──────┘ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ PARALLEL │ │ 5-TIER LLM │ │ WIKIPEDIA │ │ SERPAPI │ │ FETCH │ │ CASCADE │ │ PROXY │ │ ECHOES │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ │ │ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ┌────┴────┐ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ │DN│ │WH│ │WB│ │Ge│ │Ge│ │OR│ │Wi│ │Di│ │Ex│ │S1│ │S2│ │S3│ │S │ │O │ │M │ │mm│ │nA│ │ │ │ki│ │sa│ │tr│ │ │ │ │ │ │ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ ▲ ▲ ▲ ▲ ▲ ▲ │ │ │ │ │ │ │ │ │ │ │ └──── OpenRouter (free tier) │ │ │ │ └───────── Google GenAI │ │ │ └────────────── Ollama · Gemma 4 31B (primary) │ │ └───────────────────── Wayback CDX API │ └────────────────────────── WHOIS (IP2WHOIS → RDAP → XML) └─────────────────────────────── DNS (A/AAAA record) ┌─────────────────────────┐ │ THE SPIRIT PERSISTS │ └────────────┬────────────┘ │ ┌────────────────────────┼────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ MONGODB │ │ TIGERDATA │ │ BACKBOARD │ │ ─────── │ │ ─────── │ │ ─────── │ │ ghost_ │ │ ghost_ │ │ semantic │ │ profiles │ │ events │ │ memory │ │ (GridFS │ │ (hyper- │ │ (recall │ │ audio) │ │ table) │ │ across │ │ │ │ │ │ summons │ └───────────┘ └───────────┘ └───────────┘ ┌──────────────────────────────────────────────────┐ │ SENTRY · DIEGETIC LEDGER │ │ ───────────────────────────────────────────── │ │ kind:paranormal · system:gate · STILL_BREATHING │ │ BACKBOARD_FADED · ELEVEN_QUOTA · GENAI_TIMEOUT │ └──────────────────────────────────────────────────┘ ┌────────────────────────────┐ │ ELEVENLABS · THE VOICE │ │ ────────────────────── │ │ Adam · flash_v2 model │ │ truncated ≤ 500 chars │ │ audio archived → GridFS │ └────────────────────────────┘ 

The Ritual, Layer by Layer

  1. The Altar (Vercel).
    React 19 + TypeScript + Vite 8. The user types a domain into the input form. When they hit Enter, App.tsx fires POST /api/recon against the Render backend via VITE_API_URL.

  2. The Necropolis (Render).
    A persistent Express server wraps every Vercel-style handler — recon, conjure, wiki, echoes — because the cascade frequently exceeds Vercel's 10-second serverless limit. Each endpoint runs with a hard timeout and a try/catch that always returns JSON.

  3. Recon → the parallel fan-out.
    Four independent systems race in parallel: DNS (via dns.promises.resolve), WHOIS (three-layer fallback: IP2WHOIS → RDAP → WhoisXMLAPI), Wayback CDX (querying the last snapshot before the death year), and Wikipedia (Action API with disambiguation filtering — because "Vine" is both a social network and a plant).

  4. The Refuse-to-Lie Gate.
    All four signals must agree the domain is dead before the ritual can proceed. If DNS still resolves or WHOIS expires in 2027, the gate fires HTTP 422 · still_breathing and the ghost never appears. The refusal is logged in Sentry with kind: paranormal · system: gate.

  5. Conjure → the 5-tier cascade.
    When the gate lets the ritual through, Mastra orchestrates a declarative fallback: Gemma 4 31B via Ollama (primary) → Google GenAI → OpenRouter → deterministic template. Each tier runs with a 20-second timeout and emits a Mastra span. The user never sees a failed summon.

  6. The Voice.
    ElevenLabs' Adam voice narrates the ghost. Audio is truncated to 500 characters (credit saver), synthesized via eleven_flash_v2 (0.5 credits/char), and archived immediately to MongoDB GridFS — replaying a ghost never touches the API again.

  7. Persistence.
    Three independent stores capture the ritual: MongoDB Atlas for the full profile + narrative + GridFS audio, TigerData for a TimescaleDB hypertable of summon events (for analytics), and Backboard for semantic memory so the ghost remembers you next time.

  8. Sentry, the diegetic ledger.
    Every subsystem failure — Backboard timeout, ElevenLabs quota, GenAI timeout, gate refusal — is captured with kind: paranormal and rendered in the UI as part of the story. 85% of Sentry events in production are diegetic warnings, not errors. Zero fatal crashes.

Read the full architecture deep-dive → ARCHITECTURE.md

The stack (21 technologies):

Layer Technology
Frontend React 19, TypeScript, Vite 8, Tailwind CSS v4
Hosting Vercel (frontend), Render (backend)
LLM Gemma 4 31B via Ollama Cloud, Google GenAI, OpenRouter
Agent orchestration Mastra
Voice ElevenLabs (Adam voice)
Data Wikipedia API, Internet Archive CDX, RDAP, IP2WHOIS, WhoisXMLAPI
Persistence MongoDB Atlas (with GridFS for audio), TigerData (TimescaleDB)
Memory Backboard
Observability Sentry (Node.js + React)
Search SerpApi

File structure

ossuary/ ├── api/ │ ├── recon.ts # DNS + WHOIS + Wayback + Wikipedia + gate │ ├── conjure.ts # 5-tier cascade + TTS + persistence │ ├── wiki.ts # Wikipedia proxy (CORS bypass) │ ├── whois.ts # 3-layer WHOIS fallback │ └── echoes.ts # SerpApi search echoes ├── lib/ │ ├── mongodb.ts # Serverless-safe MongoDB client │ ├── persist.ts # Ghost profile + audio archival │ └── tigerdata.ts # Hypertable client for analytics ├── src/ │ ├── App.tsx # Main React component (~1300 lines) │ ├── agent/index.ts # Mastra agents + 5-tier fallbacks │ └── sentry/diegetic.ts # Diegetic error-to-narrative mapping ├── server-production.ts # Express wrapper for Render └── render.yaml # Deployment blueprint 

Key code excerpt — the refusal gate

// api/recon.ts — Refuse-to-Lie invariant function determineLifeStatus( lastAlive: string | undefined, wayback: { hasSnapshot: boolean; closestTimestamp?: string } ): { status: 'alive' | 'deceased'; reason: string } { // Wikipedia confirms death → deceased if (lastAlive) { return { status: 'deceased', reason: 'Wikipedia confirms the service shut down.' }; } // No snapshot at all → deceased if (!wayback.hasSnapshot || !wayback.closestTimestamp) { return { status: 'deceased', reason: 'No Wayback snapshot found.' }; } // Recent snapshot (< 2 years) → alive, refuse the summon const year = parseInt(wayback.closestTimestamp.slice(0, 4), 10); const currentYear = new Date().getFullYear(); if (year >= currentYear - 2) { return { status: 'alive', reason: 'Recent snapshot found. Still breathing.' }; } return { status: 'deceased', reason: `Last snapshot is from ${year}.` }; } // In the handler: const lifeCheck = determineLifeStatus(lastAlive, waybackResult); if (lifeCheck.status === 'alive') { return res.status(422).json({ error: 'still_breathing', reason: lifeCheck.reason, }); } 

The gate is not "an if statement at the top of the file." It's a discipline enforced at three layers (recon, conjure, wiki proxy) and validated on every deploy through a 60-check pre-demo verification script.

How I Built It

I want to walk through each technology honestly — including the parts that didn't work out of the box — because this article is also a review of what I'd do differently next time. No hype. No sponsor-flattery.

Mastra — The Silent Conductor

What it does for OSSUARY: The 5-tier LLM cascade needed real orchestration, not just a try/catch. Mastra lets me declare agents (profilerAgent, voiceAgent, and their fallbacks) declaratively. It handles retries, timeouts, and model handoffs.

The good: Once set up, the cascade just works. Tier 1 (Ollama) has served 100% of production requests. I have never once seen a user-facing failure because Mastra caught it before it could surface.

The honest part: Mastra is heavy for a project this size. If I only had one LLM tier, a plain fetch() with a timeout would be simpler. I reached for Mastra because I specifically wanted to demonstrate a cascade — and it earned its weight. But it's not a framework you install casually.

Gemma 4 + Ollama Cloud — The Voice of the Dead

What it does for OSSUARY: Gemma 4 31B is the primary brain, served through Ollama Cloud at 600–2800ms per narrative. The narratives it produces are genuinely good — good enough to make you feel slightly bad for a defunct e-commerce site.

The good: The fact that Gemma is open-weight matters philosophically. A project about preserving digital heritage shouldn't itself be a black box. When Gemma says "I am the ghost of a luxury empire," you can inspect the weights that produced that sentence.

The honest part: Ollama Cloud is fast but occasionally throttled. It's not the same as running Gemma locally — you don't get the "works with the lights off" guarantee because you're still hitting a remote endpoint. For a real offline séance, I'd want local inference on a GPU. That's the v2 plan.

ElevenLabs — The Credit Burner (Affectionately)

What it does for OSSUARY: The Adam voice narrates every ghost. When pets.com says "I spent my life selling loyalty to the highest bidder" in that voice, it lands.

The honest part, and this is a big one: the pricing model ate me alive on day one. eleven_multilingual_v2 charges 1 credit per character. A single ghost narrative runs 800–1000 characters. That's 1000 credits per summon. My free tier was 10,000. I burned 1,900 credits in my first hour of testing.

Here's what I now do:

  • Switched to eleven_flash_v2 (0.5 credits/char)
  • Truncate text to 500 characters before sending (halves the cost again)
  • Added DISABLE_ELEVENLABS=true env var so I can test the UI without touching the API
  • Built a Browser TTS fallback (Web Speech API) so the ghost still speaks even when ElevenLabs is disabled

A full summon now costs ~250 credits instead of ~1000. And every audio file is archived to MongoDB GridFS immediately — replaying a ghost's voice costs zero API calls.

The lesson: ElevenLabs is worth the cost if you plan for it. But you have to plan for it.

When i want to claim the credits : All codes have been claimed for now. (that doesnt makes me surrender at all!)

Sentry — The Diegetic Ledger

What it does for OSSUARY: Sentry is not just an error tracker here — it's a narrative element.

Every subsystem failure gets a tag kind: paranormal. When Backboard times out, Sentry logs BACKBOARD_FADED. When ElevenLabs hits a quota, ELEVEN_QUOTA. When the refuse-to-lie gate refuses a domain, STILL_BREATHING with system gate. Then the UI reads those logs and turns them into story beats.

In 7 days of production, Sentry captured 40 events across 10 distinct issues. 85% of those were diegetic warnings, not errors. Zero fatal crashes.

The honest part: I hit a Sentry quirk early on. The browser SDK adds baggage and sentry-trace headers to every fetch for distributed tracing. My backend's CORS policy didn't allow those headers, and the browser silently blocked every single API request. The fix was one line (Access-Control-Allow-Headers: Content-Type, baggage, sentry-trace) but it took three hours to find. If you're using Sentry in the browser and hitting mysterious CORS failures — check your Allow-Headers.

i know... i know... this is not the way to use sentry, you can check the other posts of mine using sentry "The Case of the Phantom DOM"

MongoDB Atlas — The Archive

What it does for OSSUARY: Every ghost profile and narrative is archived in MongoDB. Every audio file goes into GridFS (chunked in 255KB pieces). Every SerpApi echo is cached for 24 hours.

The good: GridFS was the smartest decision of the project. ElevenLabs audio is ~1.4MB per ghost. Storing that as base64 inside a document would hit the 16MB limit and bloat the collection. GridFS handles it properly, and replaying a ghost's voice costs zero API calls.

The honest part: Free tier (M0) has 512MB storage — around 200 ghost audio files. Fine for a demo, not fine for a product. Also, the connection pool needs to be small (maxPoolSize: 5) on serverless — a beginner mistake that would burn through connections fast.

TigerData — The Ledger

What it does for OSSUARY: Every summon is recorded in a TimescaleDB hypertable. Fields: event_time, domain, event_type, tier_used, duration_ms, success, error_code.

The good: The data is inherently time-series. A classic Postgres B-tree is fine for "find all events for pets.com" — but for "show me p99 latency of tier 1 over the last hour," you need a hypertable. That's what TigerData gave me.

The honest part: I've only begun to use the advanced features. Continuous aggregates are planned for v2. Right now TigerData is a glorified event log. Also the SSL setup took a couple of tries — TigerData uses self-signed certs and Node's pg driver rejects them by default. Fix was ssl: { rejectUnauthorized: false } — but that's something you should never do in real production. In v2, I'd pin the actual CA.

Backboard — The Memory

What it does for OSSUARY: Every ghost remembers its past summons. Backboard stores semantic context — emotional state, last-seen year, sensory detail — so a ghost summoned twice can reference its previous incarnation.

The good: This is the feature that makes OSSUARY feel alive. Summon pets.com once, and the ghost introduces itself. Summon it again next week, and it says "you again? still haunting me?" That continuity matters. It's the difference between a random story generator and a persistent entity.

The honest part: Backboard occasionally times out. When it does, Sentry logs BACKBOARD_FADED and the ghost falls back to a cold-start narrative. In 7 days, this happened 13 times — the most frequent diegetic event. Not a bug, but the weakest link in the persistence chain.

SerpApi — The Echoes

What it does for OSSUARY: Every ghost is accompanied by three "echoes from the search index" — the top Google results for its domain. This grounds the ghost in reality. When you read about pets.com's death, you can click through to a 2009 article about the bankruptcy.

The honest part: 250 free searches per month. Enough for a demo, not for a public launch. Cache aggressively.

Render — The Backend Necropolis

What it does for OSSUARY: Hosts the production Express backend that runs every API handler.

The good: I moved from Vercel serverless because the conjure cascade regularly exceeded Vercel's 10-second function limit. A Gemma call takes 5 seconds, TTS another 5, MongoDB write another 1 — that's 11 seconds before you blink. Render gives me a persistent Express server with real connections to MongoDB and TigerData. No cold starts on the DB side, no connection pooling nightmares.

The honest part: Render's free tier sleeps after 15 minutes of inactivity. First request after sleep takes ~30 seconds to warm. That's catastrophic for a demo. Fix: an external uptime monitor (cron-job.org) pings /health every 5 minutes. Well-known workaround, but a workaround — I'd happily pay $7/month for Starter if this were a real product.

Vercel — The Altar

What it does for OSSUARY: Hosts the React frontend with zero-config deploys, preview URLs per branch, and 40-second build times.

The honest part: Vercel is great for the frontend and painful for anything with cold-start-sensitive backend logic. Keep them separate. My VITE_API_URL env var lets me swap between Vercel functions and Render backends without rebuilding.

Why Does Open Innovation Matter?

I could have built this with GPT-4. I chose not to, for three reasons.

1. Open weights are honest. A project about remembering the dead shouldn't itself be a black box. When Gemma 4 says "I am the ghost of a luxury empire," you can inspect the weights that produced that sentence. With GPT-4, you can't.

2. The cascade needs freedom. A 5-tier fallback only makes sense if the tiers are pluggable. With closed APIs, you're locked into one vendor's rate limits, one vendor's uptime. With Gemma/Ollama/OpenRouter, I can swap any tier for any other and the cascade keeps working.

3. The economics work. OSSUARY ran 200+ narrative generations during development. At GPT-4 pricing, that would have been $40+. At Ollama Cloud free tier, it was $0. At Gemma local (v2 plan), it will still be $0. Free-tier accessibility matters for a Hacktoberfest project.

The one closed-source piece of the stack is ElevenLabs, and I'm honest about why: there is no open-weight TTS model that sounds as good as Adam. If there were, I'd use it.

My Agent Session

OSSUARY
You

PROJECT BRIEF: OSSUARY — The Last Packet. Digital necromancy tool. User types a dead domain. App performs three reconnaissance operations in parallel: TCP probe via net.Socket handshake (3.5s timeout), RDAP lookup over HTTPS (4.5s timeout), Wayback Machine availability query (4.5s timeout). The combined footprint drives a two-stage LLM pipeline: Stage 1 reconstructs a ghost profile (JSON schema: original_purpose, last_seen_year, cause_of_death enum, emotional_state enum, one_sensory_detail); Stage 2 writes a 150-200 word monologue in second person, ending with a dark joke. ElevenLabs voices the monologue with voice 'Sam – Dark and Scary' (vflhOcwDlonnVFj3wJqz). Backboard persists each summoning for continuity. Sentry does double duty: standard observability AND diegetic telemetry — every timeout, ECONNREFUSED, or rate-limit is surfaced in the UI as a paranormal event, tagged with { kind: 'paranormal' }. STACK (locked): React+Vite frontend, Express on Vercel Functions, Mastra for agent orchestration, Gemma 3 via Ollama Cloud, Backboard, Sentry, ElevenLabs, Vercel. Architecture: split into POST /api/recon (target <5s, max 30s) and POST /api/conjure (target <45s, max 60s). Non-negotiable constraints: (1) Every external call must use AbortSignal.timeout() with an explicit fallback path. (2) Every Sentry.captureMessage must include tags: { kind: 'paranormal' }. (3) All user inputs sanitized against injection (whitelist, not blacklist). (4) Rate-limit all public endpoints via in-memory sliding window. (5) Assume I will paste code without editing — no partial snippets. First deliverable: reality check and top-3 risks ranked by severity × likelihood. If the plan is unachievable within 48 hours, say so immediately so I can cut scope. Then answer the five open questions: (Q1) Mastra + Ollama Cloud Agent instantiation pattern; (Q2) Backboard API shape; (Q3) timeout strategy for 27B cold starts; (Q4) Vercel response size limits for base64 audio; (Q5) Sentry serverless initialization pattern. Finally, deliver working code for api/recon.ts, api/conjure.ts, src/agent/index.ts, vercel.json.

Show more
Agent

REALITY CHECK: Achievable within 48 hours if and only if audio generation and memory persistence are treated as non-blocking side-effects, never as critical-path blockers. The two-phase endpoint split is structurally correct — attempting two sequential 27B model calls plus synchronous ElevenLabs TTS inside a single Vercel Hobby function will routinely exceed the 60-second ceiling and produce HTTP 504s. The winning architecture is a fast-fallback cascade that degrades to a deterministic monologue rather than failing.

TOP 3 RISKS (ranked by severity × likelihood):
(1) CRITICAL — Cold-start 504 cascade in /api/conjure. Ollama Cloud 27B cold starts frequently take 18–30 seconds to first token. Two sequential calls plus TTS easily exceed 60s. Mitigation: bound Stage 1 to 14s and Stage 2 to 16s via Promise.race, cascade to gemma2:9b (3–5s cold start), then deterministic template.
(2) HIGH — Serverless network state is unreliable. Raw ICMP ping via child_process fails in Vercel Lambda due to missing CAP_NET_RAW permission. The whois-json package shells out to an OS binary that does not exist in serverless. Mitigation: replace ICMP with net.Socket TCP handshake on port 80/443 (userland, no privilege requirement); replace whois-json with RDAP over HTTPS at rdap.org/domain/{domain}.
(3) MEDIUM — ElevenLabs free tier exhaustion. 10,000 characters/month ≈ 50 summonings. A burst of judge testing will exhaust credits in minutes. Mitigation: bound TTS to 8s, return audio: null with diegetic notice 'The ghost has lost its voice.' on failure.

ANSWERS:
Q1 (Mastra + Ollama Cloud): Mastra consumes Vercel AI SDK LanguageModel instances. The correct pattern for an OpenAI-compatible remote endpoint is @ai-sdk/openai-compatible: createOpenAICompatible({ name: 'ollama', baseURL: 'https://ollama.com/v1', apiKey: process.env.OLLAMA_API_KEY }), then provider('gemma3:27b'). Pass into Agent({ model }).
Q2 (Backboard): POST https://app.backboard.io/api/threads/messages with header X-API-Key and body { content: string, memory: 'Auto', assistant_id?: string, thread_id?: string }. With memory: 'Auto', Backboard extracts facts automatically at the assistant level without requiring a manual vector store.
Q3 (Timeout strategy): Do not attempt client-side streaming across two serverless hops — the connection cannot be held open reliably on Vercel Hobby. Use an in-function timeout cascade with fast model fallback instead: gemma3:27b (14s/16s) → gemma2:9b (10s/12s) → deterministic hardcoded monologue.
Q4 (Vercel limit): 4.5 MB hard limit on both request and response bodies. A 150–200 word MP3 is ~150–250 KB binary, ~200–330 KB as base64 in JSON — under 8% of the limit. No Vercel Blob required.
Q5 (Sentry): Initialize @sentry/node at the top of each api/*.ts file before any other import. Critical detail: Vercel freezes the Lambda container immediately upon res.end() — unflushed events are dropped. You MUST await Sentry.flush(2000) inside every catch block and before every return.

Delivering full code for all requested files now.

Show more
You

First retest revealed a critical fabrication bug. When I typed google.com (a live domain), the app summoned a ghost with a fabricated narrative claiming the site 'died in 2026'. This violates the project's core invariant — the ossuary refuses to lie. Root cause is architectural, not a rendering bug: /api/conjure has no gate, and VOICE_SYSTEM_PRIMARY literally says 'You are the digital ghost of a dead domain', so Gemma fabricates death for live sites regardless of footprint evidence. Fix: add a gate that runs BEFORE the LLM is invoked and refuses conjuration when the domain appears alive. Return a diegetic 422 with stillAlive: true. Preset relics (geocities.com, vine.co, pets.com) MUST bypass via a force: true flag — otherwise geocities.com (which now redirects to a Yahoo parking page) will be refused even though it is culturally dead. Manual text input must NOT bypass. Also: the Vite dev server has no /api proxy — requests to /api/recon return index.html with status 200, res.json() crashes, the catch block fires, and a hardcoded fallback stub appears. Fix both in one pass.

Show more
Agent

Fixed both issues. (1) Living-domain gate v1 inserted in api/conjure.ts immediately after sanitizeFootprint succeeds and before the LLM cascade begins. Logic: const currentYear = new Date().getFullYear(); const waybackYear = sanitized.wayback.closestTimestamp ? parseInt(sanitized.wayback.closestTimestamp.slice(0, 4), 10) : 0; const force = body?.force === true; const looksAlive = !force && sanitized.ping.alive === true && sanitized.whois.status === 'active' && (!sanitized.wayback.hasSnapshot || waybackYear >= currentYear - 2). On refusal: log Sentry event with tags.kind=paranormal, await Sentry.flush(1500), return 422 with { error: 'The ritual failed. ${domain} is still breathing.', stillAlive: true }. The onPreset handler in App.tsx now passes force: true; the manual input handler does not. (2) vite.config.ts adds server.proxy for /api to http://localhost:3001 with changeOrigin: true. Requires running vercel dev --listen 3001 alongside bun run dev, or using vercel dev alone. README updated.

One architectural note: this gate strengthens the submission rather than weakening it. The refusal becomes a second narrative beat in the demo — type dev.to, watch the gate refuse ('still breathing'), then click the geocities.com preset and watch the ghost appear. The live-vs-dead contrast is now the emotional core of the product, not just a feature.

Show more
You

Second retest failed. google.com was STILL being summoned as a ghost even with the gate in place. Root cause: the gate depends on ping.alive === true, but net.Socket TCP probes to port 80 silently time out in Vercel Lambda due to network egress restrictions on certain IPs. So ping.alive is false even for definitively live domains, and the gate never fires. Fix: the gate must be independent of the TCP probe entirely. Rely on WHOIS status and Wayback recency — both are authoritative HTTPS sources that always return. Include the evidence in the 422 response body (whoisStatus, waybackYear, pingAlive) so I can verify why it fired. Second issue: I cannot tell which LLM tier is answering because all fallbacks are silent. Add a TierAttempt[] diagnostics array to the response so the UI can render which provider answered, with status, durationMs, and error. This is non-negotiable — a silent fallback chain is worse than no fallback chain because it is untestable.

Show more
Agent

Fixed. Two architectural corrections applied.

(1) Gate v2 — independent of the TCP probe. New logic: const whoisSaysAlive = sanitized.whois.status === 'active'; const whoisSaysDead = sanitized.whois.status === 'expired' || sanitized.whois.status === 'unregistered'; const waybackRecent = waybackYear > 0 && waybackYear >= currentYear - 2; const looksAlive = !force && (whoisSaysAlive || (waybackRecent && !whoisSaysDead)). Fires reliably for google.com, dev.to, facebook.com even in serverless. Returns 422 with evidence: { whoisStatus, waybackYear, pingAlive }. Preset relics bypass with force: true. The lesson is worth documenting explicitly: serverless environments lie about network state — never gate a critical decision on a TCP probe when authoritative HTTPS sources are available.

(2) TierAttempt[] diagnostics. Added interface TierAttempt { tier: string; provider: string; model: string; status: 'success' | 'timeout' | 'failed' | 'skipped'; durationMs: number; error?: string; }. Refactored generateProfile and generateNarrative to accept tierLog: TierAttempt[] and push a structured entry on every success and failure path. Missing API keys produce status: 'skipped' with explicit error ('GEMINI_API_KEY not configured', 'OPENROUTER_API_KEY not configured'). The response now includes tiers: tierLog. GhostScreen renders this as a collapsible 'conjuration tier log' section with color-coded status per row (green success, dim skipped, amber timeout, blood red failed). Debugging the cascade is now a first-class feature, not a hidden chore.

Show more
You

The UI shipped in v1 is functional but reads like a monitoring dashboard — two-column Tailwind grid, debug info in the sidebar. That is the opposite of ritual. Treat the UI as an altar, not a control panel. v2 attempt with monospace typography, gr...

Source:https://dev.to/ggle_in/every-ai-horror-project-lies-to-you-i-built-one-that-refuses-1ji1

Related posts