Alain Lilic

LiveRestaurant and hotelBottrop, Germany

Haus Wessels / Hotel Nike: website, operations cockpit and Telegram ops channel

Haus Wessels / Hotel Nike homepage in Bottrop on desktop: photo of a wooden door in a brick arch, headline about steaks from the open fire and rooms with breakfast, navigation to restaurant, hotel, menu, rooms, celebrations and reservations.
Haus Wessels / Hotel Nike homepage on a smartphone: image of the wooden door, headline, phone button and menu button.

Problem

Haus Wessels is a family-run steakhouse restaurant with a hotel in Bottrop. Bookings arrive by phone, email, the website and on handwritten notes, supplier invoices come on paper, guest emails land in a shared inbox. The old website was more than ten years old. The business needed a new website and a tool that works from a phone during service.

My role

I own the project on my own: requirements, data model, architecture, security model, prompts, tests and deployment. I cut the work into build steps, have every step reviewed, test against a copy of the production data and decide what goes live.

Approach

I built the website with Next.js 16 and Payload 3.88 on SQLite. The business maintains the menu (allergens are a required field), rooms, floor plans with tables and inquiries in the CMS. The Next.js/Payload system with backend, cockpit and API runs in Docker behind Caddy on a VPS. The public site is served as a verified static snapshot on the hotel's existing hosting. Publishing is a git push: a hook builds, checks the snapshot and uploads only the changed files.

Day to day there are two surfaces: the cockpit at /verwaltung and a Telegram bot on my voice and chat agent platform. In the bot the team asks about bookings in plain language, sends photos of notes and invoices, or has replies to guest emails drafted. A vision model reads the photos, a language model writes the text. One read function feeds the cockpit, the counters and Telegram, plus a daily report at 11 am. Writes only go through the hotel's signed /api/v1, and only after someone taps a card.

Architecture

  1. Input

    Website form, email in the hotel inbox, photo, text or voice message in Telegram.

  2. Bot on my agent platform (Python)

    Language model with read tools, vision extraction for receipts and notes, a mail watcher that only reads the inbox.

  3. Card in Telegram

    Draft with the exact content and buttons. A person taps Eintragen (book) or Senden (send).

  4. Signed /api/v1

    29 routes. API key with scope plus HMAC over timestamp, method, path and raw body, idempotency key, rate limit per client.

  5. Payload and cockpit

    Bookings, receipts, mail journal and audit log stay with the hotel. Cockpit with Today, inquiries, mail, receipts, deadlines.

  6. Operations

    Docker Compose behind Caddy. Mail attachments only through a sanitizer container without network access. Public static snapshot via git push.

Decision

The model reads freely but only writes drafts

The bot may query the booking book, mail, receipts and deadlines freely. Anything that writes becomes a Telegram card showing the exact content. Only a tap on Eintragen or Senden triggers the signed call, with an idempotency key against double taps. The model writes, the person decides. I learned that a tap alone is not enough when a draft once went out unchecked. Since then the send button carries a fingerprint of the text it shows, and both the bot and the hotel check every guest email for instructions to the AI, salutation and closing before it goes out. The one deliberate exception to the card rule is a short note on a receipt.

Code excerpt

core/betriebsfunk.py (shortened)Scroll sidewayspython
async def _werkzeug(agent: str, profil: dict, chat_id: str, name: str, args: dict,
                    notizen: list[str], *, wirt: str = "", ...) -> dict:
    """Einen Werkzeugaufruf ausführen. Lesen geht direkt ans Buch; alles, was
    schreibt, wird eine Karte (und eine Notiz fürs Gedächtnis), nur ein
    Vermerk am Beleg wirkt sofort. ..."""
    # ...
    if name == "entwurf_anlegen":
        absicht = {k: args.get(k) for k in _ENTWURF_FELDER}
        fehlt = _entwurf_fehlt(absicht)
        if fehlt:
            return {"fehlt": fehlt, "hinweis": "Keine Karte gesendet, erst nachfragen."}
        daten = _entwurf_daten(absicht)
        entwurf_id = await _karte_senden(agent, profil, chat_id, daten)
        # ...
        return {"entwurf_id": entwurf_id, "karte": "gesendet"}

async def _eintragen(agent: str, profil: dict, ereignis, entwurf_id: int,
                     *, trotzdem: bool) -> None:
    # ...
    idempotenz = f"telegram:{entwurf_id}" + (":tz" if trotzdem else "")
    try:
        antwort = await [...].reservierung_melden(
            profil, _nutzlast(daten, entwurf_id, trotzdem=trotzdem),
            idempotenz=idempotenz)
    # ...
Read tools go straight to the hotel's booking book. A write request from the model only becomes a card. Only the tap on Eintragen calls the signed API, with an idempotency key against double taps. One module name is left out.

Results

  • 29routes under /api/v1, each with API key, scope and HMAC signature
  • 9scopes, split by area and by read or write
  • 5cards at most per block on the Today page, previously 67 cards across 15 screens
  • 10/10correct references in a re-test with the real model after the prompt hardening on 13 Oct 2026
17
test scripts in the hotel repo, plus a signed smoke test against production

What I learned

I should have built the check before guest emails go out from the start, not only after a mistake. The first photo extraction had no measurement and was barely usable for real notes: the model did not know today's date, and several entries on one photo broke the parser. Today I build the eval per category first, with one rule: correct or flagged as uncertain, never silently wrong. And invoices are still linked to stays only through note text. I would add a proper field to the data model from the start.

Screenshots

Haus Wessels restaurant page: headline 'Steakhaus-Restaurant Haus Wessels', a short text about the kitchen, buttons for the menu and for table reservations, next to a photo of the building's facade.
hotel-nike-haus-wessels.de/restaurant