Case studies / Haunts

Haunts: a local guide kept current by an AI pipeline and a human editor

Margate's independent venues post their hours, closures and events on Instagram, often only inside a poster or a photo of a chalkboard. Haunts reads every post with AI and turns it into a guide you can trust, with a person approving each change.

Haunts home screen: tide and sunset times, then Margate places to eat with today's opening hours
Haunts What's on screen: gallery shows with posters, marked Ends today and Last chance
Fig. 1 Haunts at haunts.fyi: today's places to eat with their hours, and what's on at the galleries, all read from venues' own posts.
What
A guide to independent Margate venues, on iPhone, Android and the web
Built by
Nathan Poulton, Karnworks, with Claude Code
AI
Gemini 2.5 Flash reads every post; a curator approves what it finds
Status
Live at haunts.fyi, 174 venues

The problem

Is that café open on a Monday in February? In Margate, the answer is usually in an Instagram post from three weeks ago. Directory sites go stale because nobody has time to keep hundreds of listings current by hand.

The information is also hard for software to read. It sits inside images as often as in captions: a photographed chalkboard, a poster in a decorative font, a carousel where the third slide carries the date. Captions and images disagree. Some posts are adverts, some are a week old, and some come from an account two businesses share.

How it works

  1. 1 Collect Public Instagram posts, via gaffa.dev
  2. 2 Read Gemini 2.5 Flash turns images and caption into listings
  3. 3 Review Confident, unflagged changes go straight through; the rest wait for the curator
  4. 4 Publish App and website, with tide and sunset times
Supervisor in build Checks each proposed change against its post and flags doubt
Fig. 2 How a post becomes a listing. The top row runs every day; the dashed stage is being built from the decisions the curator has already made.
  1. Collect. Every day, each venue’s public Instagram posts are collected through gaffa.dev, a browser automation API. Captions and images come through together.
  2. Read. Gemini 2.5 Flash reads the images and the caption as one piece of evidence and returns structured listings: opening hours, closures, events with dates and times, and offers.
  3. Review. A curator sees each proposed change beside its source post and accepts, edits or rejects it. Nothing reaches the guide without that approval.
  4. Publish. Approved changes go to the iPhone and Android apps and the website, alongside local extras such as sunset and tide times.

Every decision the curator makes is kept. Those decisions are how the system is measured and improved.

What the AI is really doing

Reading a poster is the easy part. The hard part is deciding what a post means for a guide that already exists, and knowing when not to trust what it reads. For every post, the system has to settle questions like these:

  • Is this new, or already listed? The same gig gets posted three times in a week. It should appear once.
  • Is this a one-off, or a change? “Closed this Monday for a private party” is not new opening hours.
  • How sure can it be? A guide that is confidently wrong is worse than one that admits it doesn’t know.
  • Is this a trap? Plenty of posts look like listings and aren’t.
Post from Where Else?, Cliftonville

Where Else?, Cliftonville · Monday 11 May 2026

“Wednesday: Open Mic hosted by @fruitbaand … Friday: @infinitebisous @morsels.website are bringing an assortment of projects … Saturday: Margate Noise Complaints with @french_toast_band @yoodoowrong @_miserablebastard_ @_tarmac”

  1. Music · new eventOpen Mic, hosted by FruitbaandWed 13 May
  2. Other · new eventInfinite Bisous and Morsels: projects from inner and other worldsFri 15 May
  3. Music · update to a listed eventMargate Noise ComplaintsSat 16 May
Fig. 3 One post, three listings. The model splits a week's line-up into separate events, and spots that Saturday's gig is already in the guide, so it updates that listing instead of adding a duplicate.

The line-up above is one kind of post. These are some of the others it now handles:

  • A venue posts about its sister bar across town. Nothing is added to the wrong venue.
  • A gig in London, promoted by a Margate venue. It stays out of a Margate guide.
  • “Something big coming soon…” with no date. It waits until there is something to list.
  • A Monday closure for a private party. The listing shows the closure; the regular hours stay as they are.
  • A post on 1 April. It gets a second look.

Getting the prompt right

The instructions the model works from are where most of the effort went. They have been through around 30 rounds of changes since March, each one against real posts the model had got wrong.

% accepted as proposed · one mark = 2 · scale starts at 60%

  1. 1 Several events per post
  2. 2 Flags and test set
  3. 3 Repeat posts merged
  4. 4 Opening hours reworked
Fig. 4 Share of the model's proposals the curator approved unchanged, by month.

Each change was made against real posts the model had got wrong, and kept only if it did better on the test set below. Line-up posters are a good example: they now become one listing per night, and of the multi-event posts reviewed in full, about three in four had every listing approved.

Today 85% of the model’s reviewed proposals are accepted exactly as written, up from 71% in April. The curator edits only 2 to 4 in every hundred; the rest are approved or rejected outright.

None of this is guesswork. A test set of 49 real posts, including line-ups, closures, opening hours and posts that should produce nothing, is re-run before any change to the instructions ships. A new prompt, or a new model, has to beat the last one on the same posts.

Choosing the model

Model choice decides both what the guide gets wrong and what it costs to run. The factors that mattered:

  • Reading text inside images. Stylised fonts, handwriting and photographs of signs are the normal case here.
  • Handling a whole post at once. Several images and a caption have to be weighed together, including when they conflict.
  • Dependable structured output. The next step is software, so the model has to return data in the same shape every time.
  • Behaviour under doubt. A model that says “this post does not give a date” is worth more than one that supplies a plausible date.
  • Cost and speed at volume. Every venue posts, every week, so the price per post matters more than the price per test.

The design this points to has two tiers. A fast, economical model does the reading on every post, which is what runs today. A more capable supervisor model will take the smaller number of judgements where a mistake would be published. That keeps running costs in proportion and puts the expensive reasoning where it changes the outcome.

Built on gaffa.dev

Collecting the posts is the part of a project like this that normally eats weeks: pages built to be hard for machines to read, rate limits, and infrastructure to keep alive. With gaffa.dev it took two prompts in a coding agent to get a working ingestion, and their team was quick to respond whenever a question came up.

The collection has been tuned since. It skips posts already stored, fetches a post shared by two venues only once, and runs four requests at a time so the daily run finishes sooner. In October an A/B test of request settings led to leaner page requests, and venues are now checked only when they’re due. Together those cut the daily cost of the run by about 70%.

It has held up in daily use. In the 12 daily runs since run-level logging began on 7 October, Gaffa handled 1,898 page requests across 1,209 venue checks without a single venue being missed; the occasional temporary error was recovered by an automatic retry.

collection cost per stored post, July = 100 · one mark = 5

  1. 1 No duplicate fetches
  2. 2 Four requests at a time
  3. 3 Leaner requests, venues checked when due
Fig. 5 How much it costs to collect each new post, relative to July.

How we built it

Haunts was built by one person working with Claude Code, Anthropic’s coding agent, on the latest Claude models. The design was shaped with Claude Design and Impeccable, so screens were argued over before they were built. The technical side, for those who want it:

  • App and website: Next.js and React in TypeScript, styled with Tailwind and shadcn/ui. The same code ships as the website and, wrapped with Capacitor, as the iPhone and Android apps.
  • Data: Supabase (Postgres), with every post, proposal and decision stored.
  • Collection: a Python scraper calling Gaffa, run on a daily schedule.
  • AI: Gemini 2.5 Flash through its API.
  • Testing: unit tests with Vitest, browser tests with Playwright, and the 49-post test set for the AI.

What carries into client work

Most businesses have their own version of this problem: information arriving as emails, PDFs, photos and forms that someone retypes into a system. The pattern is the same. Turn unstructured input into structured data, check it automatically, and bring a person in for the approvals and the exceptions.

The full technical write-up of how Haunts makes these judgements is available on a call.

Where it is now

Haunts is Karnworks’ own product, built and run by Nathan Poulton. It is live on the App Store, on Google Play and at haunts.fyi, and is being introduced to Margate venues and community groups.

Got paperwork that follows a pattern?

An hour's chat costs nothing. We'll find where the quick wins are in your business.

Talk to us →